diff options
437 files changed, 10662 insertions, 4334 deletions
@@ -1,6 +1,7 @@ source 'http://rubygems.org' gem "arel", :git => "git://github.com/rails/arel.git" +#gem "rack-mount", :git => "git://github.com/rails/rack-mount.git" gem "rails", :path => File.dirname(__FILE__) gem "rake", ">= 0.8.7" diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 9e3437685d..11fa978b9c 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -4,6 +4,7 @@ require 'action_mailer/collector' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/proc' +require 'action_mailer/log_subscriber' module ActionMailer #:nodoc: # Action Mailer allows you to send email from your application using a mailer model and views. @@ -182,7 +183,7 @@ module ActionMailer #:nodoc: # end # # Which will (if it had both a <tt>welcome.text.plain.erb</tt> and <tt>welcome.text.html.erb</tt> - # tempalte in the view directory), send a complete <tt>multipart/mixed</tt> email with two parts, + # template in the view directory), send a complete <tt>multipart/mixed</tt> email with two parts, # the first part being a <tt>multipart/alternative</tt> with the text and HTML email parts inside, # and the second being a <tt>application/pdf</tt> with a Base64 encoded copy of the file.pdf book # with the filename +free_book.pdf+. @@ -395,7 +396,7 @@ module ActionMailer #:nodoc: end # Wraps an email delivery inside of Active Support Notifications instrumentation. This - # method is actually called by the <tt>Mail::Message</tt> object itself through a call back + # method is actually called by the <tt>Mail::Message</tt> object itself through a callback # when you call <tt>:deliver</tt> on the Mail::Message, calling +deliver_mail+ directly # and passing a Mail::Message will do nothing except tell the logger you sent the email. def deliver_mail(mail) #:nodoc: @@ -534,7 +535,9 @@ module ActionMailer #:nodoc: # :reply_to => 'bounces@test.lindsaar.net' # end # - # If you need other headers not listed above, use the <tt>headers['name'] = value</tt> method. + # If you need other headers not listed above, you can either pass them in + # as part of the headers hash or use the <tt>headers['name'] = value</tt> + # method. # # When a <tt>:return_path</tt> is specified as header, that value will be used as the 'envelope from' # address for the Mail message. Setting this is useful when you want delivery notifications diff --git a/actionmailer/lib/action_mailer/deprecated_api.rb b/actionmailer/lib/action_mailer/deprecated_api.rb index c08ab4164e..0070d8e016 100644 --- a/actionmailer/lib/action_mailer/deprecated_api.rb +++ b/actionmailer/lib/action_mailer/deprecated_api.rb @@ -1,7 +1,7 @@ module ActionMailer # This is the API which is deprecated and is going to be removed on Rails 3.1 release. # Part of the old API will be deprecated after 3.1, for a smoother deprecation process. - # Chech those in OldApi instead. + # Check those in OldApi instead. module DeprecatedApi #:nodoc: extend ActiveSupport::Concern diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb new file mode 100644 index 0000000000..7ba57b19e0 --- /dev/null +++ b/actionmailer/lib/action_mailer/log_subscriber.rb @@ -0,0 +1,22 @@ +require 'active_support/core_ext/array/wrap' + +module ActionMailer + class LogSubscriber < ActiveSupport::LogSubscriber + def deliver(event) + recipients = Array.wrap(event.payload[:to]).join(', ') + info("\nSent mail to #{recipients} (%1.fms)" % event.duration) + debug(event.payload[:mail]) + end + + def receive(event) + info("\nReceived mail (%.1fms)" % event.duration) + debug(event.payload[:mail]) + end + + def logger + ActionMailer::Base.logger + end + end +end + +ActionMailer::LogSubscriber.attach_to :action_mailer
\ No newline at end of file diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 43a4936013..d7b09b2dc6 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -5,9 +5,6 @@ module ActionMailer class Railtie < Rails::Railtie config.action_mailer = ActiveSupport::OrderedOptions.new - require "action_mailer/railties/log_subscriber" - log_subscriber :action_mailer, ActionMailer::Railties::LogSubscriber.new - initializer "action_mailer.logger" do ActiveSupport.on_load(:action_mailer) { self.logger ||= Rails.logger } end diff --git a/actionmailer/lib/action_mailer/railties/log_subscriber.rb b/actionmailer/lib/action_mailer/railties/log_subscriber.rb deleted file mode 100644 index af76d807d0..0000000000 --- a/actionmailer/lib/action_mailer/railties/log_subscriber.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'active_support/core_ext/array/wrap' - -module ActionMailer - module Railties - class LogSubscriber < Rails::LogSubscriber - def deliver(event) - recipients = Array.wrap(event.payload[:to]).join(', ') - info("\nSent mail to #{recipients} (%1.fms)" % event.duration) - debug(event.payload[:mail]) - end - - def receive(event) - info("\nReceived mail (%.1fms)" % event.duration) - debug(event.payload[:mail]) - end - - def logger - ActionMailer::Base.logger - end - end - end -end diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb index c08c34b4a2..ba9b4d6500 100644 --- a/actionmailer/test/log_subscriber_test.rb +++ b/actionmailer/test/log_subscriber_test.rb @@ -1,13 +1,13 @@ require "abstract_unit" -require "rails/log_subscriber/test_helper" -require "action_mailer/railties/log_subscriber" +require "active_support/log_subscriber/test_helper" +require "action_mailer/log_subscriber" class AMLogSubscriberTest < ActionMailer::TestCase - include Rails::LogSubscriber::TestHelper + include ActiveSupport::LogSubscriber::TestHelper def setup super - Rails::LogSubscriber.add(:action_mailer, ActionMailer::Railties::LogSubscriber.new) + ActionMailer::LogSubscriber.attach_to :action_mailer end class TestMailer < ActionMailer::Base diff --git a/actionmailer/test/old_base/mail_service_test.rb b/actionmailer/test/old_base/mail_service_test.rb index 527b37218a..831adf3e32 100644 --- a/actionmailer/test/old_base/mail_service_test.rb +++ b/actionmailer/test/old_base/mail_service_test.rb @@ -674,7 +674,7 @@ The body EOF mail = Mail.new(msg) assert_equal "testing testing \326\244", mail.subject - assert_equal "Subject: testing testing =?UTF-8?Q?_=D6=A4=?=\r\n", mail[:subject].encoded + assert_equal "Subject: =?UTF-8?Q?testing_testing_=D6=A4?=\r\n", mail[:subject].encoded end def test_unquote_7bit_subject @@ -863,7 +863,7 @@ EOF def test_multipart_with_utf8_subject mail = TestMailer.multipart_with_utf8_subject(@recipient) - regex = Regexp.escape('Subject: Foo =?UTF-8?Q?=C3=A1=C3=AB=C3=B4=?= =?UTF-8?Q?_=C3=AE=C3=BC=?=') + regex = Regexp.escape('Subject: =?UTF-8?Q?Foo_=C3=A1=C3=AB=C3=B4_=C3=AE=C3=BC?=') assert_match(/#{regex}/, mail.encoded) string = "Foo áëô îü" assert_match(string, mail.subject) @@ -871,7 +871,7 @@ EOF def test_implicitly_multipart_with_utf8 mail = TestMailer.implicitly_multipart_with_utf8 - regex = Regexp.escape('Subject: Foo =?UTF-8?Q?=C3=A1=C3=AB=C3=B4=?= =?UTF-8?Q?_=C3=AE=C3=BC=?=') + regex = Regexp.escape('Subject: =?UTF-8?Q?Foo_=C3=A1=C3=AB=C3=B4_=C3=AE=C3=BC?=') assert_match(/#{regex}/, mail.encoded) string = "Foo áëô îü" assert_match(string, mail.subject) diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index a00f63dcd4..32aba2091a 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,22 @@ *Rails 3.0.0 [Release Candidate] (unreleased)* +* link_to, button_to, and tag/tag_options now rely on html_escape instead of escape_once. [fxn] + +* url_for returns always unescaped strings, and the :escape option is gone. [fxn] + +* Added accept-charset parameter and _snowman hidden field to force the contents + of Rails POSTed forms to be in UTF-8 [Yehuda Katz] + +* Upgrade to Rack 1.2.1 [Jeremy Kemper] + +* Allow :path to be given to match/get/post/put/delete instead of :path_names in the new router [Carlos Antônio da Silva] + +* Added resources_path_names to the new router DSL [José Valim] + +* Allow options to be given to the namespace method in the new router [Carlos Antônio da Silva] + +* Deprecate :name_prefix in the new router DSL [José Valim] + * Add shallow routes back to the new router [Diego Carrion, Andrew White] resources :posts do @@ -14,7 +31,6 @@ * Removed textilize, textilize_without_paragraph and markdown helpers. [Santiago Pastorino] - *Rails 3.0.0 [beta 4] (June 8th, 2010)* * Remove middleware laziness [José Valim] diff --git a/actionpack/README b/actionpack/README index 8bdcb9120a..1a59f728cc 100644 --- a/actionpack/README +++ b/actionpack/README @@ -183,20 +183,20 @@ A short rundown of the major features: * Automated benchmarking and integrated logging - Processing WeblogController#index (for 127.0.0.1 at Fri May 28 00:41:55) - Parameters: {"action"=>"index", "controller"=>"weblog"} - Rendering weblog/index (200 OK) - Completed in 0.029281 (34 reqs/sec) + Started GET "/weblog" for 127.0.0.1 at Fri May 28 00:41:55 + Processing by WeblogController#index as HTML + Rendered weblog/index.html.erb within layouts/application (25.7ms) + Completed 200 OK in 29.3ms If Active Record is used as the model, you'll have the database debugging as well: - Processing PostsController#create (for 127.0.0.1 at Sat Jun 19 14:04:23) - Params: {"controller"=>"posts", "action"=>"create", - "post"=>{"title"=>"this is good"} } - SQL (0.000627) INSERT INTO posts (title) VALUES('this is good') + Started POST "/posts" for 127.0.0.1 at Sat Jun 19 14:04:23 + Processing by PostsController#create as HTML + Parameters: {"post"=>{"title"=>"this is good"}} + SQL (0.6ms) INSERT INTO posts (title) VALUES('this is good') Redirected to http://example.com/posts/5 - Completed in 0.221764 (4 reqs/sec) | DB: 0.059920 (27%) + Completed 302 Found in 221ms (Views: 215ms | ActiveRecord: 0.6ms) You specify a logger through a class method, such as: diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 8e95315252..b4311599a9 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -23,9 +23,9 @@ Gem::Specification.new do |s| s.add_dependency('activemodel', version) s.add_dependency('builder', '~> 2.1.2') s.add_dependency('i18n', '~> 0.4.1') - s.add_dependency('rack', '~> 1.1.0') + s.add_dependency('rack', '~> 1.2.1') s.add_dependency('rack-test', '~> 0.5.4') - s.add_dependency('rack-mount', '~> 0.6.4') + #s.add_dependency('rack-mount', '~> 0.6.6') s.add_dependency('tzinfo', '~> 0.3.16') s.add_dependency('erubis', '~> 2.6.5') end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index e1027840ef..8a8337858b 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -1,4 +1,5 @@ require 'active_support/configurable' +require 'active_support/descendants_tracker' require 'active_support/core_ext/module/anonymous' module AbstractController @@ -10,6 +11,7 @@ module AbstractController attr_internal :action_name include ActiveSupport::Configurable + extend ActiveSupport::DescendantsTracker class << self attr_reader :abstract @@ -21,17 +23,6 @@ module AbstractController @abstract = true end - def inherited(klass) - ::AbstractController::Base.descendants << klass.to_s - super - end - - # A list of all descendents of AbstractController::Base. This is - # useful for initializers which need to add behavior to all controllers. - def descendants - @descendants ||= [] - end - # A list of all internal methods for a controller. This finds the first # abstract superclass of a controller, and gets a list of all public # instance methods on that abstract class. Public instance methods of diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 8611d0d3c3..a70ba0d2e3 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,3 +1,5 @@ +require "action_controller/log_subscriber" + module ActionController class Base < Metal abstract! diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb new file mode 100644 index 0000000000..ece270b3ce --- /dev/null +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -0,0 +1,56 @@ +require 'active_support/core_ext/object/blank' + +module ActionController + class LogSubscriber < ActiveSupport::LogSubscriber + INTERNAL_PARAMS = %w(controller action format _method only_path) + + def start_processing(event) + payload = event.payload + params = payload[:params].except(*INTERNAL_PARAMS) + + info " Processing by #{payload[:controller]}##{payload[:action]} as #{payload[:formats].first.to_s.upcase}" + info " Parameters: #{params.inspect}" unless params.empty? + end + + def process_action(event) + payload = event.payload + additions = ActionController::Base.log_process_action(payload) + + message = "Completed #{payload[:status]} #{Rack::Utils::HTTP_STATUS_CODES[payload[:status]]} in %.0fms" % event.duration + message << " (#{additions.join(" | ")})" unless additions.blank? + + info(message) + end + + def send_file(event) + message = "Sent file %s" + message << " (%.1fms)" + info(message % [event.payload[:path], event.duration]) + end + + def redirect_to(event) + info "Redirected to #{event.payload[:location]}" + end + + def send_data(event) + info("Sent data %s (%.1fms)" % [event.payload[:filename], event.duration]) + end + + %w(write_fragment read_fragment exist_fragment? + expire_fragment expire_page write_page).each do |method| + class_eval <<-METHOD, __FILE__, __LINE__ + 1 + def #{method}(event) + key_or_path = event.payload[:key] || event.payload[:path] + human_name = #{method.to_s.humanize.inspect} + info("\#{human_name} \#{key_or_path} (%.1fms)" % event.duration) + end + METHOD + end + + def logger + ActionController::Base.logger + end + end +end + +ActionController::LogSubscriber.attach_to :action_controller
\ No newline at end of file diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 775a5002e2..2281c500c5 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -52,8 +52,7 @@ module ActionController class Metal < AbstractController::Base abstract! - # :api: public - attr_internal :params, :env + attr_internal :env # Returns the last part of the controller's name, underscored, without the ending # "Controller". For instance, MyApp::MyPostsController would return "my_posts" for @@ -62,7 +61,7 @@ module ActionController # ==== Returns # String def self.controller_name - @controller_name ||= controller_path.split("/").last + @controller_name ||= self.name.demodulize.sub(/Controller$/, '').underscore end # Delegates to the class' #controller_name @@ -85,6 +84,14 @@ module ActionController super end + def params + @_params ||= request.parameters + end + + def params=(val) + @_params = val + end + # Basic implementations for content_type=, location=, and headers are # provided to reduce the dependency on the RackDelegation module # in Renderer and Redirector. diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index b6d4fb1284..89201fb5ee 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -58,12 +58,12 @@ module ActionController module ClassMethods def helpers_dir - ActiveSupport::Deprecation.warn "helpers_dir is deprecated, use helpers_path instead" + ActiveSupport::Deprecation.warn "helpers_dir is deprecated, use helpers_path instead", caller self.helpers_path end def helpers_dir=(value) - ActiveSupport::Deprecation.warn "helpers_dir= is deprecated, use helpers_path= instead" + ActiveSupport::Deprecation.warn "helpers_dir= is deprecated, use helpers_path= instead", caller self.helpers_path = Array(value) end diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb index 508ea6e2b7..544b4989c7 100644 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ b/actionpack/lib/action_controller/metal/rack_delegation.rb @@ -14,10 +14,6 @@ module ActionController super(action, request) end - def params - @_params ||= @_request.parameters - end - def response_body=(body) response.body = body if response super diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 8c25b147ef..b632e7aab6 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -55,7 +55,7 @@ module ActionController #:nodoc: config_accessor :request_forgery_protection_token self.request_forgery_protection_token ||= :authenticity_token - # Controls whether request forgergy protection is turned on or not. Turned off by default only in test mode. + # Controls whether request forgery protection is turned on or not. Turned off by default only in test mode. config_accessor :allow_forgery_protection self.allow_forgery_protection = true if allow_forgery_protection.nil? diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index 22bdcd0f3c..cb644dfd16 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -89,9 +89,7 @@ module ActionController #:nodoc: def initialize(controller, resources, options={}) @controller = controller - @request = controller.request - @format = controller.formats.first - @resource = resources.is_a?(Array) ? resources.last : resources + @resource = resources.last @resources = resources @options = options @action = options.delete(:action) @@ -101,6 +99,14 @@ module ActionController #:nodoc: delegate :head, :render, :redirect_to, :to => :controller delegate :get?, :post?, :put?, :delete?, :to => :request + def request + @request ||= @controller.request + end + + def format + @format ||= @controller.formats.first + end + # Undefine :to_json and :to_yaml since it's defined on Object undef_method(:to_json) if method_defined?(:to_json) undef_method(:to_yaml) if method_defined?(:to_yaml) @@ -147,7 +153,7 @@ module ActionController #:nodoc: elsif has_errors? && default_action render :action => default_action else - redirect_to resource_location + redirect_to navigation_location end end @@ -160,7 +166,7 @@ module ActionController #:nodoc: elsif has_errors? display resource.errors, :status => :unprocessable_entity elsif post? - display resource, :status => :created, :location => resource_location + display resource, :status => :created, :location => api_location else head :ok end @@ -178,6 +184,8 @@ module ActionController #:nodoc: def resource_location options[:location] || resources end + alias :navigation_location :resource_location + alias :api_location :resource_location # If a given response block was given, use it, otherwise call render on # controller. diff --git a/actionpack/lib/action_controller/polymorphic_routes.rb b/actionpack/lib/action_controller/polymorphic_routes.rb index 7f2eb4306b..bee50a7a3b 100644 --- a/actionpack/lib/action_controller/polymorphic_routes.rb +++ b/actionpack/lib/action_controller/polymorphic_routes.rb @@ -11,7 +11,7 @@ module ActionController # polymorphic_url([:admin, @article, @comment]) # # results in: - # + # # admin_article_comment_url(@article, @comment) # # == Usage within the framework @@ -166,6 +166,7 @@ module ActionController route << RecordIdentifier.__send__("plural_class_name", record) route = route.singularize if inflection == :singular route << "_" + route << "index_" if RecordIdentifier.uncountable?(record) && inflection == :plural end action_prefix(options) + route + routing_type(options).to_s diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 0e3cdffadc..86395c4d93 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -6,7 +6,6 @@ require "active_support/core_ext/class/subclasses" require "active_support/deprecation/proxy_wrappers" require "active_support/deprecation" -require "action_controller/railties/log_subscriber" require "action_controller/railties/url_helpers" module ActionController @@ -35,8 +34,6 @@ module ActionController end end - log_subscriber :action_controller, ActionController::Railties::LogSubscriber.new - initializer "action_controller.set_configs" do |app| paths = app.config.paths ac = app.config.action_controller diff --git a/actionpack/lib/action_controller/railties/log_subscriber.rb b/actionpack/lib/action_controller/railties/log_subscriber.rb deleted file mode 100644 index 00ac3bdf67..0000000000 --- a/actionpack/lib/action_controller/railties/log_subscriber.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'active_support/core_ext/object/blank' - -module ActionController - module Railties - class LogSubscriber < Rails::LogSubscriber - INTERNAL_PARAMS = %w(controller action format _method only_path) - - def start_processing(event) - payload = event.payload - params = payload[:params].except(*INTERNAL_PARAMS) - - info " Processing by #{payload[:controller]}##{payload[:action]} as #{payload[:formats].first.to_s.upcase}" - info " Parameters: #{params.inspect}" unless params.empty? - end - - def process_action(event) - payload = event.payload - additions = ActionController::Base.log_process_action(payload) - - message = "Completed #{payload[:status]} #{Rack::Utils::HTTP_STATUS_CODES[payload[:status]]} in %.0fms" % event.duration - message << " (#{additions.join(" | ")})" unless additions.blank? - - info(message) - end - - def send_file(event) - message = "Sent file %s" - message << " (%.1fms)" - info(message % [event.payload[:path], event.duration]) - end - - def redirect_to(event) - info "Redirected to #{event.payload[:location]}" - end - - def send_data(event) - info("Sent data %s (%.1fms)" % [event.payload[:filename], event.duration]) - end - - %w(write_fragment read_fragment exist_fragment? - expire_fragment expire_page write_page).each do |method| - class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def #{method}(event) - key_or_path = event.payload[:key] || event.payload[:path] - human_name = #{method.to_s.humanize.inspect} - info("\#{human_name} \#{key_or_path} (%.1fms)" % event.duration) - end - METHOD - end - - def logger - ActionController::Base.logger - end - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb index 907c369218..d20c3b64c5 100644 --- a/actionpack/lib/action_controller/record_identifier.rb +++ b/actionpack/lib/action_controller/record_identifier.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/module' module ActionController - # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or + # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or # Active Resources or pretty much any other model type that has an id. These patterns are then used to try elevate # the view actions to a higher logical level. Example: # @@ -28,7 +28,7 @@ module ActionController # end # # As the example above shows, you can stop caring to a large extent what the actual id of the post is. You just know - # that one is being assigned and that the subsequent calls in redirect_to and the RJS expect that same naming + # that one is being assigned and that the subsequent calls in redirect_to and the RJS expect that same naming # convention and allows you to write less code if you follow it. module RecordIdentifier extend self @@ -59,7 +59,7 @@ module ActionController # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id: # # dom_id(Post.find(45), :edit) # => "edit_post_45" - def dom_id(record, prefix = nil) + def dom_id(record, prefix = nil) if record_id = record_key_for_dom_id(record) "#{dom_class(record, prefix)}#{JOIN}#{record_id}" else @@ -102,6 +102,14 @@ module ActionController model_name_from_record_or_class(record_or_class).singular end + # Identifies whether the class name of a record or class is uncountable. Examples: + # + # uncountable?(Sheep) # => true + # uncountable?(Post) => false + def uncountable?(record_or_class) + plural_class_name(record_or_class) == singular_class_name(record_or_class) + end + private def model_name_from_record_or_class(record_or_class) (record_or_class.is_a?(Class) ? record_or_class : record_or_class.class).model_name diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 21281b606e..650eb16ac0 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -40,7 +40,7 @@ module ActionController ActiveSupport::Notifications.unsubscribe("!render_template.action_view") end - # Asserts that the request was rendered with the appropriate template file or partials + # Asserts that the request was rendered with the appropriate template file or partials. # # ==== Examples # @@ -53,6 +53,12 @@ module ActionController # # assert that no partials were rendered # assert_template :partial => false # + # In a view test case, you can also assert that specific locals are passed + # to partials: + # + # # assert that the "_customer" partial was rendered with a specific object + # assert_template :partial => '_customer', :locals => { :customer => @customer } + # def assert_template(options = {}, message = nil) validate_request! @@ -72,9 +78,13 @@ module ActionController end when Hash if expected_partial = options[:partial] - if expected_count = options[:count] + if expected_locals = options[:locals] + actual_locals = @locals[expected_partial.to_s.sub(/^_/,'')] + expected_locals.each_pair do |k,v| + assert_equal(v, actual_locals[k]) + end + elsif expected_count = options[:count] actual_count = @partials[expected_partial] - # actual_count = found.nil? ? 0 : found[1] msg = build_message(message, "expecting ? to be rendered ? time(s) but rendered ? time(s)", expected_partial, expected_count, actual_count) @@ -183,12 +193,14 @@ module ActionController replace(session.stringify_keys) @loaded = true end + + def exists?; true; end end # Superclass for ActionController functional tests. Functional tests allow you to # test a single controller action per test method. This should not be confused with # integration tests (see ActionController::IntegrationTest), which are more like - # "stories" that can involve multiple controllers and mutliple actions (i.e. multiple + # "stories" that can involve multiple controllers and multiple actions (i.e. multiple # different HTTP requests). # # == Basic example @@ -442,7 +454,7 @@ module ActionController end # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline - # (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular + # (skipping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular # rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else # than 0.0.0.0. # diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb index 6c0331636c..a874519978 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb @@ -177,6 +177,7 @@ module HTML #:nodoc: case text when "\\" then value << text + break if scanner.eos? value << scanner.getch when delim break diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb index 064ff3724d..240dc1890f 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb @@ -96,6 +96,7 @@ module HTML #:nodoc: while match = @scanner.scan_until(/[\\#{delim}]/) text << match break if @scanner.matched == delim + break if @scanner.eos? text << @scanner.getch # skip the escaped character end end diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 9b9e81440b..e9fdf75cc8 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -89,7 +89,7 @@ module ActionDispatch if etag? || last_modified? || !@cache_control.empty? set_conditional_cache_control! elsif nonempty_ok_response? - self.etag = @body + self.etag = body if request && request.etag_matches?(etag) self.status = 304 @@ -137,4 +137,4 @@ module ActionDispatch end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index d6a805bf3b..c6fc582851 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -60,7 +60,7 @@ module Mime def initialize(order, name, q=nil) @order = order @name = name.strip - q ||= 0.0 if @name == Mime::ALL # default wilcard match to end of list + q ||= 0.0 if @name == Mime::ALL # default wildcard match to end of list @q = ((q || 1.0).to_f * 100).to_i end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index bc43414e75..add8cab2ab 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -6,7 +6,11 @@ module ActionDispatch module Parameters # Returns both GET and POST \parameters in a single hash. def parameters - @env["action_dispatch.request.parameters"] ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access + @env["action_dispatch.request.parameters"] ||= begin + params = request_parameters.merge(query_parameters) + params.merge!(path_parameters) + encode_params(params).with_indifferent_access + end end alias :params :parameters @@ -32,7 +36,32 @@ module ActionDispatch end private - # Convert nested Hashs to HashWithIndifferentAccess + + # TODO: Validate that the characters are UTF-8. If they aren't, + # you'll get a weird error down the road, but our form handling + # should really prevent that from happening + def encode_params(params) + return params unless "ruby".encoding_aware? + + if params.is_a?(String) + return params.force_encoding("UTF-8").encode! + elsif !params.is_a?(Hash) + return params + end + + params.each do |k, v| + case v + when Hash + encode_params(v) + when Array + v.map! {|el| encode_params(el) } + else + encode_params(v) + end + end + end + + # Convert nested Hash to HashWithIndifferentAccess def normalize_parameters(value) case value when Hash diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 98f4f5ae18..fd23b1df79 100755..100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -194,9 +194,12 @@ module ActionDispatch @env['rack.input'] end + # TODO This should be broken apart into AD::Request::Session and probably + # be included by the session middleware. def reset_session - self.session_options.delete(:id) + session.destroy if session self.session = {} + @env['action_dispatch.request.flash_hash'] = nil end def session=(session) #:nodoc: diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 81d2517304..8ee4b81cdd 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -31,8 +31,8 @@ module ActionDispatch end module Upload - # Convert nested Hashs to HashWithIndifferentAccess and replace - # file upload hashs with UploadedFile objects + # Convert nested Hash to HashWithIndifferentAccess and replace + # file upload hash with UploadedFile objects def normalize_parameters(value) if Hash === value && value.has_key?(:tempfile) upload = value[:tempfile] diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index d07841218a..e4ae480bfb 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -8,7 +8,7 @@ module ActionDispatch class Callbacks include ActiveSupport::Callbacks - define_callbacks :call, :terminator => "result == false", :rescuable => true + define_callbacks :call, :rescuable => true define_callbacks :prepare, :scope => :name # Add a preparation callback. Preparation callbacks are run before every @@ -37,12 +37,12 @@ module ActionDispatch def initialize(app, prepare_each_request = false) @app, @prepare_each_request = app, prepare_each_request - run_callbacks(:prepare) + _run_prepare_callbacks end def call(env) - run_callbacks(:call) do - run_callbacks(:prepare) if @prepare_each_request + _run_call_callbacks do + _run_prepare_callbacks if @prepare_each_request @app.call(env) end end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 18771fe782..bfa30cf5af 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -4,7 +4,7 @@ module ActionDispatch # read a notice you put there or <tt>flash["notice"] = "hello"</tt> # to put a new one. def flash - session['flash'] ||= Flash::FlashHash.new + @env['action_dispatch.request.flash_hash'] ||= (session["flash"] || Flash::FlashHash.new) end end @@ -176,7 +176,14 @@ module ActionDispatch @app.call(env) ensure - if (session = env['rack.session']) && session.key?('flash') && session['flash'].empty? + session = env['rack.session'] || {} + flash_hash = env['action_dispatch.request.flash_hash'] + + if flash_hash && (!flash_hash.empty? || session.key?('flash')) + session["flash"] = flash_hash + end + + if session.key?('flash') && session['flash'].empty? session.delete('flash') end end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 3e8d64b0c6..08bc80dbc2 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -12,6 +12,35 @@ module ActionDispatch ENV_SESSION_KEY = 'rack.session'.freeze ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze + # thin wrapper around Hash that allows us to lazily + # load session id into session_options + class OptionsHash < Hash + def initialize(by, env, default_options) + @by = by + @env = env + @session_id_loaded = false + merge!(default_options) + end + + def [](key) + if key == :id + load_session_id! unless super(:id) || has_session_id? + end + super(key) + end + + private + + def has_session_id? + @session_id_loaded + end + + def load_session_id! + self[:id] = @by.send(:extract_session_id, @env) + @session_id_loaded = true + end + end + class SessionHash < Hash def initialize(by, env) super() @@ -21,66 +50,75 @@ module ActionDispatch end def [](key) - load! unless @loaded + load_for_read! + super(key.to_s) + end + + def has_key?(key) + load_for_read! super(key.to_s) end def []=(key, value) - load! unless @loaded + load_for_write! super(key.to_s, value) end def to_hash + load_for_read! h = {}.replace(self) h.delete_if { |k,v| v.nil? } h end def update(hash) - load! unless @loaded + load_for_write! super(hash.stringify_keys) end def delete(key) - load! unless @loaded + load_for_write! super(key.to_s) end def inspect - load! unless @loaded + load_for_read! super end + def exists? + return @exists if instance_variable_defined?(:@exists) + @exists = @by.send(:exists?, @env) + end + def loaded? @loaded end + def destroy + clear + @by.send(:destroy, @env) if @by + @env[ENV_SESSION_OPTIONS_KEY][:id] = nil if @env && @env[ENV_SESSION_OPTIONS_KEY] + @loaded = false + end + private - def load! - stale_session_check! do - id, session = @by.send(:load_session, @env) - (@env[ENV_SESSION_OPTIONS_KEY] ||= {})[:id] = id - replace(session.stringify_keys) - @loaded = true - end + + def load_for_read! + load! if !loaded? && exists? end - def stale_session_check! - yield - rescue ArgumentError => argument_error - if argument_error.message =~ %r{undefined class/module ([\w:]*\w)} - begin - # Note that the regexp does not allow $1 to end with a ':' - $1.constantize - rescue LoadError, NameError => const_error - raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" - end - - retry - else - raise - end + def load_for_write! + load! unless loaded? + end + + def load! + id, session = @by.send(:load_session, @env) + @env[ENV_SESSION_OPTIONS_KEY][:id] = id + replace(session.stringify_keys) + @loaded = true end + end DEFAULT_OPTIONS = { @@ -108,8 +146,8 @@ module ActionDispatch session_data = env[ENV_SESSION_KEY] options = env[ENV_SESSION_OPTIONS_KEY] - if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after] - session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?) + if !session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after] + session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded? sid = options[:id] || generate_sid session_data = session_data.to_hash @@ -133,7 +171,7 @@ module ActionDispatch def prepare!(env) env[ENV_SESSION_KEY] = SessionHash.new(self, env) - env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup + env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options) end def generate_sid @@ -141,15 +179,30 @@ module ActionDispatch end def set_cookie(request, options) - request.cookie_jar[@key] = options + if request.cookie_jar[@key] != options[:value] || !options[:expires].nil? + request.cookie_jar[@key] = options + end end def load_session(env) - request = Rack::Request.new(env) - sid = request.cookies[@key] - sid ||= request.params[@key] unless @cookie_only - sid, session = get_session(env, sid) - [sid, session] + stale_session_check! do + sid = current_session_id(env) + sid, session = get_session(env, sid) + [sid, session] + end + end + + def extract_session_id(env) + stale_session_check! do + request = ActionDispatch::Request.new(env) + sid = request.cookies[@key] + sid ||= request.params[@key] unless @cookie_only + sid + end + end + + def current_session_id(env) + env[ENV_SESSION_OPTIONS_KEY][:id] end def ensure_session_key! @@ -161,6 +214,26 @@ module ActionDispatch end end + def stale_session_check! + yield + rescue ArgumentError => argument_error + if argument_error.message =~ %r{undefined class/module ([\w:]*\w)} + begin + # Note that the regexp does not allow $1 to end with a ':' + $1.constantize + rescue LoadError, NameError => const_error + raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" + end + retry + else + raise + end + end + + def exists?(env) + current_session_id(env).present? + end + def get_session(env, sid) raise '#get_session needs to be implemented.' end @@ -169,6 +242,10 @@ module ActionDispatch raise '#set_session needs to be implemented and should return ' << 'the value to be stored in the cookie (usually the sid)' end + + def destroy(env) + raise '#destroy needs to be implemented.' + end end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 92a86ee229..dce47c63bc 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -39,16 +39,6 @@ module ActionDispatch # # Note that changing digest or secret invalidates all existing sessions! class CookieStore < AbstractStore - class OptionsHash < Hash - def initialize(by, env, default_options) - @session_data = env[AbstractStore::ENV_SESSION_KEY] - merge!(default_options) - end - - def [](key) - key == :id ? @session_data[:session_id] : super(key) - end - end def initialize(app, options = {}) super(app, options.merge!(:cookie_only => true)) @@ -57,19 +47,32 @@ module ActionDispatch private - def prepare!(env) - env[ENV_SESSION_KEY] = SessionHash.new(self, env) - env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options) - end - def load_session(env) - request = ActionDispatch::Request.new(env) - data = request.cookie_jar.signed[@key] + data = unpacked_cookie_data(env) data = persistent_session_id!(data) - data.stringify_keys! [data["session_id"], data] end + def extract_session_id(env) + if data = unpacked_cookie_data(env) + data["session_id"] + else + nil + end + end + + def unpacked_cookie_data(env) + env["action_dispatch.request.unsigned_session_cookie"] ||= begin + stale_session_check! do + request = ActionDispatch::Request.new(env) + if data = request.cookie_jar.signed[@key] + data.stringify_keys! + end + data || {} + end + end + end + def set_cookie(request, options) request.cookie_jar.signed[@key] = options end @@ -78,6 +81,10 @@ module ActionDispatch persistent_session_id!(session_data, sid) end + def destroy(env) + # session data is stored on client; nothing to do here + end + def persistent_session_id!(data, sid=nil) data ||= {} data["session_id"] ||= sid || generate_sid diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index 8df8f977e8..28e3dbd732 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -42,6 +42,15 @@ module ActionDispatch rescue MemCache::MemCacheError, Errno::ECONNREFUSED false end + + def destroy(env) + if sid = current_session_id(env) + @pool.delete(sid) + end + rescue MemCache::MemCacheError, Errno::ECONNREFUSED + false + end + end end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 0a6d2bfc8a..e095b51342 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -122,7 +122,7 @@ module ActionDispatch end def render(status, body) - [status, {'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s}, [body]] + [status, {'Content-Type' => 'text/html', 'Content-Length' => body.bytesize.to_s}, [body]] end def public_path diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 4240e7a5d5..4618f3befc 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -70,7 +70,7 @@ module ActionDispatch end def active - ActiveSupport::Deprecation.warn "All middlewares in the chaing are active since the laziness " << + ActiveSupport::Deprecation.warn "All middlewares in the chain are active since the laziness " << "was removed from the middleware stack", caller end diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 38da44d7e7..ed93211255 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -10,7 +10,7 @@ module ActionDispatch # Prepare dispatcher callbacks and run 'prepare' callbacks initializer "action_dispatch.prepare_dispatcher" do |app| - ActionDispatch::Callbacks.to_prepare { app.routes_reloader.reload_if_changed } + ActionDispatch::Callbacks.to_prepare { app.routes_reloader.execute_if_updated } end end end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 3d9084a27e..401d98b663 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/regexp' require 'action_controller/polymorphic_routes' module ActionDispatch - # == Routing + # = Routing # # The routing module provides URL rewriting in native Ruby. It's a way to # redirect incoming requests to controllers and actions. This replaces diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb index 1b676669e2..bc3f62fb48 100644 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -28,61 +28,13 @@ module ActionDispatch end end - # Mapper instances are used to build routes. The object passed to the draw - # block in config/routes.rb is a Mapper instance. - # - # Mapper instances have relatively few instance methods, in order to avoid - # clashes with named routes. - # - # == Overview - # - # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms, - # is something that can be pointed at and it will respond with a representation of the data requested. - # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application - # requests XML data. - # - # RESTful design is based on the assumption that there are four generic verbs that a user of an - # application can request from a \resource (the noun). - # - # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used - # denotes the type of action that should take place. - # - # === The Different Methods and their Usage - # - # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request. - # * POST - Creation of \resources. - # * PUT - Editing of attributes on a \resource. - # * DELETE - Deletion of a \resource. - # - # === Examples - # - # # A GET request on the Posts resource is asking for all Posts - # GET /posts - # - # # A GET request on a single Post resource is asking for that particular Post - # GET /posts/1 - # - # # A POST request on the Posts resource is asking for a Post to be created with the supplied details - # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } - # - # # A PUT request on a single Post resource is asking for a Post to be updated - # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } - # - # # A DELETE request on a single Post resource is asking for it to be deleted - # DELETE /posts # with => { :id => 1 } - # - # By using the REST convention, users of our application can assume certain things about how the data - # is requested and how it is returned. Rails simplifies the routing part of RESTful design by - # supplying you with methods to create them in your routes.rb file. - # - # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer class DeprecatedMapper #:nodoc: def initialize(set) #:nodoc: + ActiveSupport::Deprecation.warn "You are using the old router DSL which will be removed in Rails 3.1. " << + "Please check how to update your router file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" @set = set end - # Create an unnamed route with the provided +path+ and +options+. See - # ActionDispatch::Routing for an introduction to routes. def connect(path, options = {}) options = options.dup @@ -240,17 +192,6 @@ module ActionDispatch connect(path, options) end - # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. - # Example: - # - # map.namespace(:admin) do |admin| - # admin.resources :products, - # :has_many => [ :tags, :images, :variants ] - # end - # - # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. - # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for - # Admin::TagsController. def namespace(name, options = {}, &block) if options[:namespace] with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) @@ -411,334 +352,11 @@ module ActionDispatch alias_method :nesting_path_prefix, :path end - # Creates named routes for implementing verb-oriented controllers - # for a collection \resource. - # - # For example: - # - # map.resources :messages - # - # will map the following actions in the corresponding controller: - # - # class MessagesController < ActionController::Base - # # GET messages_url - # def index - # # return all messages - # end - # - # # GET new_message_url - # def new - # # return an HTML form for describing a new message - # end - # - # # POST messages_url - # def create - # # create a new message - # end - # - # # GET message_url(:id => 1) - # def show - # # find and return a specific message - # end - # - # # GET edit_message_url(:id => 1) - # def edit - # # return an HTML form for editing a specific message - # end - # - # # PUT message_url(:id => 1) - # def update - # # find and update a specific message - # end - # - # # DELETE message_url(:id => 1) - # def destroy - # # delete a specific message - # end - # end - # - # Along with the routes themselves, +resources+ generates named routes for use in - # controllers and views. <tt>map.resources :messages</tt> produces the following named routes and helpers: - # - # Named Route Helpers - # ============ ===================================================== - # messages messages_url, hash_for_messages_url, - # messages_path, hash_for_messages_path - # - # message message_url(id), hash_for_message_url(id), - # message_path(id), hash_for_message_path(id) - # - # new_message new_message_url, hash_for_new_message_url, - # new_message_path, hash_for_new_message_path - # - # edit_message edit_message_url(id), hash_for_edit_message_url(id), - # edit_message_path(id), hash_for_edit_message_path(id) - # - # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example: - # - # redirect_to :controller => 'messages', :action => 'index' - # # and - # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> - # - # now become: - # - # redirect_to messages_url - # # and - # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically - # - # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your - # form tags. The form helpers make this a little easier. For an update form with a <tt>@message</tt> object: - # - # <%= form_tag message_path(@message), :method => :put %> - # - # or - # - # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> - # - # or - # - # <% form_for @message do |f| %> - # - # which takes into account whether <tt>@message</tt> is a new record or not and generates the - # path and method accordingly. - # - # The +resources+ method accepts the following options to customize the resulting routes: - # * <tt>:collection</tt> - Add named routes for other actions that operate on the collection. - # Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>, - # an array of any of the previous, or <tt>:any</tt> if the method does not matter. - # These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. - # * <tt>:member</tt> - Same as <tt>:collection</tt>, but for actions that operate on a specific member. - # * <tt>:new</tt> - Same as <tt>:collection</tt>, but for actions that operate on the new \resource action. - # * <tt>:controller</tt> - Specify the controller name for the routes. - # * <tt>:singular</tt> - Specify the singular name used in the member routes. - # * <tt>:requirements</tt> - Set custom routing parameter requirements; this is a hash of either - # regular expressions (which must match for the route to match) or extra parameters. For example: - # - # map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' } - # - # will only match if the first part is alphabetic, and will pass the parameter :extra to the controller. - # * <tt>:conditions</tt> - Specify custom routing recognition conditions. \Resources sets the <tt>:method</tt> value for the method-specific routes. - # * <tt>:as</tt> - Specify a different \resource name to use in the URL path. For example: - # # products_path == '/productos' - # map.resources :products, :as => 'productos' do |product| - # # product_reviews_path(product) == '/productos/1234/comentarios' - # product.resources :product_reviews, :as => 'comentarios' - # end - # - # * <tt>:has_one</tt> - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current. - # * <tt>:has_many</tt> - Same has <tt>:has_one</tt>, but for plural \resources. - # - # You may directly specify the routing association with +has_one+ and +has_many+ like: - # - # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments] - # - # This is the same as: - # - # map.resources :notes do |notes| - # notes.resource :author - # notes.resources :comments - # notes.resources :attachments - # end - # - # * <tt>:path_names</tt> - Specify different path names for the actions. For example: - # # new_products_path == '/productos/nuevo' - # # bids_product_path(1) == '/productos/1/licitacoes' - # map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' } - # - # You can also set default action names from an environment, like this: - # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } - # - # * <tt>:path_prefix</tt> - Set a prefix to the routes with required route variables. - # - # Weblog comments usually belong to a post, so you might use +resources+ like: - # - # map.resources :articles - # map.resources :comments, :path_prefix => '/articles/:article_id' - # - # You can nest +resources+ calls to set this automatically: - # - # map.resources :articles do |article| - # article.resources :comments - # end - # - # The comment \resources work the same, but must now include a value for <tt>:article_id</tt>. - # - # article_comments_url(@article) - # article_comment_url(@article, @comment) - # - # article_comments_url(:article_id => @article) - # article_comment_url(:article_id => @article, :id => @comment) - # - # If you don't want to load all objects from the database you might want to use the <tt>article_id</tt> directly: - # - # articles_comments_url(@comment.article_id, @comment) - # - # * <tt>:name_prefix</tt> - Define a prefix for all generated routes, usually ending in an underscore. - # Use this if you have named routes that may clash. - # - # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' - # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' - # - # You may also use <tt>:name_prefix</tt> to override the generic named routes in a nested \resource: - # - # map.resources :articles do |article| - # article.resources :comments, :name_prefix => nil - # end - # - # This will yield named \resources like so: - # - # comments_url(@article) - # comment_url(@article, @comment) - # - # * <tt>:shallow</tt> - If true, paths for nested resources which reference a specific member - # (ie. those with an :id parameter) will not use the parent path prefix or name prefix. - # - # The <tt>:shallow</tt> option is inherited by any nested resource(s). - # - # For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources: - # - # map.resources :users, :shallow => true do |user| - # user.resources :posts do |post| - # post.resources :comments - # end - # end - # # --> GET /users/1/posts (maps to the PostsController#index action as usual) - # # also adds the usual named route called "user_posts" - # # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested) - # # also adds the named route called "post" - # # --> GET /posts/2/comments (maps to the CommentsController#index action) - # # also adds the named route called "post_comments" - # # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested) - # # also adds the named route called "comment" - # - # You may also use <tt>:shallow</tt> in combination with the +has_one+ and +has_many+ shorthand notations like: - # - # map.resources :users, :has_many => { :posts => :comments }, :shallow => true - # - # * <tt>:only</tt> and <tt>:except</tt> - Specify which of the seven default actions should be routed to. - # - # <tt>:only</tt> and <tt>:except</tt> may be set to <tt>:all</tt>, <tt>:none</tt>, an action name or a - # list of action names. By default, routes are generated for all seven actions. - # - # For example: - # - # map.resources :posts, :only => [:index, :show] do |post| - # post.resources :comments, :except => [:update, :destroy] - # end - # # --> GET /posts (maps to the PostsController#index action) - # # --> POST /posts (fails) - # # --> GET /posts/1 (maps to the PostsController#show action) - # # --> DELETE /posts/1 (fails) - # # --> POST /posts/1/comments (maps to the CommentsController#create action) - # # --> PUT /posts/1/comments/1 (fails) - # - # If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied. - # - # Examples: - # - # map.resources :messages, :path_prefix => "/thread/:thread_id" - # # --> GET /thread/7/messages/1 - # - # map.resources :messages, :collection => { :rss => :get } - # # --> GET /messages/rss (maps to the #rss action) - # # also adds a named route called "rss_messages" - # - # map.resources :messages, :member => { :mark => :post } - # # --> POST /messages/1/mark (maps to the #mark action) - # # also adds a named route called "mark_message" - # - # map.resources :messages, :new => { :preview => :post } - # # --> POST /messages/new/preview (maps to the #preview action) - # # also adds a named route called "preview_new_message" - # - # map.resources :messages, :new => { :new => :any, :preview => :post } - # # --> POST /messages/new/preview (maps to the #preview action) - # # also adds a named route called "preview_new_message" - # # --> /messages/new can be invoked via any request method - # - # map.resources :messages, :controller => "categories", - # :path_prefix => "/category/:category_id", - # :name_prefix => "category_" - # # --> GET /categories/7/messages/1 - # # has named route "category_message" - # - # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an - # HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in - # <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for \resource routes. def resources(*entities, &block) options = entities.extract_options! entities.each { |entity| map_resource(entity, options.dup, &block) } end - # Creates named routes for implementing verb-oriented controllers for a singleton \resource. - # A singleton \resource is global to its current context. For unnested singleton \resources, - # the \resource is global to the current user visiting the application, such as a user's - # <tt>/account</tt> profile. For nested singleton \resources, the \resource is global to its parent - # \resource, such as a <tt>projects</tt> \resource that <tt>has_one :project_manager</tt>. - # The <tt>project_manager</tt> should be mapped as a singleton \resource under <tt>projects</tt>: - # - # map.resources :projects do |project| - # project.resource :project_manager - # end - # - # See +resources+ for general conventions. These are the main differences: - # * A singular name is given to <tt>map.resource</tt>. The default controller name is still taken from the plural name. - # * To specify a custom plural name, use the <tt>:plural</tt> option. There is no <tt>:singular</tt> option. - # * No default index route is created for the singleton \resource controller. - # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1') - # - # For example: - # - # map.resource :account - # - # maps these actions in the Accounts controller: - # - # class AccountsController < ActionController::Base - # # GET new_account_url - # def new - # # return an HTML form for describing the new account - # end - # - # # POST account_url - # def create - # # create an account - # end - # - # # GET account_url - # def show - # # find and return the account - # end - # - # # GET edit_account_url - # def edit - # # return an HTML form for editing the account - # end - # - # # PUT account_url - # def update - # # find and update the account - # end - # - # # DELETE account_url - # def destroy - # # delete the account - # end - # end - # - # Along with the routes themselves, +resource+ generates named routes for - # use in controllers and views. <tt>map.resource :account</tt> produces - # these named routes and helpers: - # - # Named Route Helpers - # ============ ============================================= - # account account_url, hash_for_account_url, - # account_path, hash_for_account_path - # - # new_account new_account_url, hash_for_new_account_url, - # new_account_path, hash_for_new_account_path - # - # edit_account edit_account_url, hash_for_edit_account_url, - # edit_account_path, hash_for_edit_account_path def resource(*entities, &block) options = entities.extract_options! entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 46304b0336..0b4ba8c9f5 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -33,7 +33,7 @@ module ActionDispatch end class Mapping #:nodoc: - IGNORE_OPTIONS = [:to, :as, :controller, :action, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow] + IGNORE_OPTIONS = [:to, :as, :controller, :action, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix] def initialize(set, scope, args) @set, @scope = set, scope @@ -55,7 +55,7 @@ module ActionDispatch path = args.first end - if @scope[:module] && options[:to] + if @scope[:module] && options[:to] && !options[:to].is_a?(Proc) if options[:to].to_s.include?("#") options[:to] = "#{@scope[:module]}/#{options[:to]}" elsif @scope[:controller].nil? @@ -102,7 +102,7 @@ module ActionDispatch end def requirements - @requirements ||= (@options[:constraints] || {}).tap do |requirements| + @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements| requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints] @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) } end @@ -131,6 +131,7 @@ module ActionDispatch end defaults[:controller] ||= default_controller + defaults[:action] ||= default_action defaults.delete(:controller) if defaults[:controller].blank? defaults.delete(:action) if defaults[:action].blank? @@ -187,6 +188,12 @@ module ActionDispatch @scope[:controller].to_s end end + + def default_action + if @options[:action] + @options[:action].to_s + end + end end # Invokes Rack::Mount::Utils.normalize path and ensure that @@ -194,7 +201,7 @@ module ActionDispatch # for root cases, where the latter is the correct one. def self.normalize_path(path) path = Rack::Mount::Utils.normalize_path(path) - path.sub!(%r{/(\(+)/?:}, '\1/:') unless path =~ %r{^/\(+:.*\)$} + path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$} path end @@ -299,6 +306,11 @@ module ActionDispatch options = args.extract_options! options = options.dup + if name_prefix = options.delete(:name_prefix) + options[:as] ||= name_prefix + ActiveSupport::Deprecation.warn ":name_prefix was deprecated in the new router syntax. Use :as instead.", caller + end + case args.first when String options[:path] = args.first @@ -341,9 +353,11 @@ module ActionDispatch scope(controller.to_sym) { yield } end - def namespace(path) + def namespace(path, options = {}) path = path.to_s - scope(:path => path, :name_prefix => path, :module => path) { yield } + options = { :path => path, :as => path, :module => path, + :shallow_path => path, :shallow_prefix => path }.merge!(options) + scope(options) { yield } end def constraints(constraints = {}) @@ -359,10 +373,10 @@ module ActionDispatch options = (@scope[:options] || {}).merge(options) - if @scope[:name_prefix] && !options[:as].blank? - options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}" - elsif @scope[:name_prefix] && options[:as] == "" - options[:as] = @scope[:name_prefix].to_s + if @scope[:as] && !options[:as].blank? + options[:as] = "#{@scope[:as]}_#{options[:as]}" + elsif @scope[:as] && options[:as] == "" + options[:as] = @scope[:as].to_s end args.push(options) @@ -378,7 +392,15 @@ module ActionDispatch Mapper.normalize_path("#{parent}/#{child}") end - def merge_name_prefix_scope(parent, child) + def merge_shallow_path_scope(parent, child) + Mapper.normalize_path("#{parent}/#{child}") + end + + def merge_as_scope(parent, child) + parent ? "#{parent}_#{child}" : child + end + + def merge_shallow_prefix_scope(parent, child) parent ? "#{parent}_#{child}" : child end @@ -403,7 +425,9 @@ module ActionDispatch end def merge_blocks_scope(parent, child) - (parent || []) + [child] + merged = parent ? parent.dup : [] + merged << child if child + merged end def merge_options_scope(parent, child) @@ -416,22 +440,27 @@ module ActionDispatch end module Resources + # CANONICAL_ACTIONS holds all actions that does not need a prefix or + # a path appended since they fit properly in their scope level. + VALID_ON_OPTIONS = [:new, :collection, :member] + CANONICAL_ACTIONS = [:index, :create, :new, :show, :update, :destroy] + MERGE_FROM_SCOPE_OPTIONS = [:shallow, :constraints] + class Resource #:nodoc: - def self.default_actions - [:index, :create, :new, :show, :update, :destroy, :edit] - end + DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit] attr_reader :controller, :path, :options def initialize(entities, options = {}) @name = entities.to_s @path = options.delete(:path) || @name - @controller = options.delete(:controller) || @name.to_s.pluralize + @controller = (options.delete(:controller) || @name).to_s + @as = options.delete(:as) @options = options end def default_actions - self.class.default_actions + self.class::DEFAULT_ACTIONS end def actions @@ -445,7 +474,7 @@ module ActionDispatch end def name - options[:as] || @name + @as || @name end def plural @@ -497,7 +526,7 @@ module ActionDispatch def nested_options {}.tap do |opts| - opts[:name_prefix] = member_name + opts[:as] = member_name opts["#{singular}_id".to_sym] = id_constraint if id_constraint? opts[:options] = { :shallow => shallow? } unless options[:shallow].nil? end @@ -515,8 +544,8 @@ module ActionDispatch ["#{path}/:id", options] end - def new_scope - [path] + def new_scope(new_path) + ["#{path}/#{new_path}"] end def nested_scope @@ -525,17 +554,20 @@ module ActionDispatch end class SingletonResource < Resource #:nodoc: - def self.default_actions - [:show, :create, :update, :destroy, :new, :edit] - end + DEFAULT_ACTIONS = [:show, :create, :update, :destroy, :new, :edit] - def initialize(entity, options = {}) - super + def initialize(entities, options) + @name = entities.to_s + @path = options.delete(:path) || @name + @controller = (options.delete(:controller) || @name.to_s.pluralize).to_s + @as = options.delete(:as) + @options = options end def member_name name end + alias :collection_name :member_name def nested_path path @@ -543,7 +575,7 @@ module ActionDispatch def nested_options {}.tap do |opts| - opts[:name_prefix] = member_name + opts[:as] = member_name opts[:options] = { :shallow => shallow? } unless @options[:shallow].nil? end end @@ -562,10 +594,12 @@ module ActionDispatch @scope[:path_names] = @set.resources_path_names end + def resources_path_names(options) + @scope[:path_names].merge!(options) + end + def resource(*resources, &block) options = resources.extract_options! - options = (@scope[:options] || {}).merge(options) - options[:shallow] = true if @scope[:shallow] && !options.has_key?(:shallow) if apply_common_behavior_for(:resource, resources, options, &block) return self @@ -575,9 +609,12 @@ module ActionDispatch yield if block_given? collection_scope do - post :create if parent_resource.actions.include?(:create) - get :new if parent_resource.actions.include?(:new) - end + post :create + end if parent_resource.actions.include?(:create) + + new_scope do + get :new + end if parent_resource.actions.include?(:new) member_scope do get :show if parent_resource.actions.include?(:show) @@ -592,8 +629,6 @@ module ActionDispatch def resources(*resources, &block) options = resources.extract_options! - options = (@scope[:options] || {}).merge(options) - options[:shallow] = true if @scope[:shallow] && !options.has_key?(:shallow) if apply_common_behavior_for(:resources, resources, options, &block) return self @@ -605,9 +640,12 @@ module ActionDispatch collection_scope do get :index if parent_resource.actions.include?(:index) post :create if parent_resource.actions.include?(:create) - get :new if parent_resource.actions.include?(:new) end + new_scope do + get :new + end if parent_resource.actions.include?(:new) + member_scope do get :show if parent_resource.actions.include?(:show) put :update if parent_resource.actions.include?(:update) @@ -644,10 +682,8 @@ module ActionDispatch raise ArgumentError, "can't use new outside resource(s) scope" end - with_scope_level(:new) do - scope(*parent_resource.new_scope) do - yield - end + new_scope do + yield end end @@ -659,10 +695,10 @@ module ActionDispatch with_scope_level(:nested) do if parent_resource.shallow? with_exclusive_scope do - if @scope[:module].blank? + if @scope[:shallow_path].blank? scope(*parent_resource.nested_scope) { yield } else - scope(@scope[:module], :name_prefix => @scope[:module].tr('/', '_')) do + scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do scope(*parent_resource.nested_scope) { yield } end end @@ -673,7 +709,7 @@ module ActionDispatch end end - def namespace(path) + def namespace(path, options = {}) if resource_scope? nested { super } else @@ -688,8 +724,7 @@ module ActionDispatch end def match(*args) - options = args.extract_options! - + options = args.extract_options!.dup options[:anchor] = true unless options.key?(:anchor) if args.length > 1 @@ -697,17 +732,12 @@ module ActionDispatch return self end - if [:collection, :member, :new].include?(options[:on]) + on = options.delete(:on) + if VALID_ON_OPTIONS.include?(on) args.push(options) - - case options.delete(:on) - when :collection - return collection { match(*args) } - when :member - return member { match(*args) } - when :new - return new { match(*args) } - end + return send(on){ match(*args) } + elsif on + raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end if @scope[:scope_level] == :resource @@ -715,14 +745,26 @@ module ActionDispatch return member { match(*args) } end - path_names = options.delete(:path_names) + path = options.delete(:path) + action = args.first - if args.first.is_a?(Symbol) - path = path_for_action(args.first, path_names) - options = options_for_action(args.first, options) + if action.is_a?(Symbol) + path = path_for_action(action, path) + options[:to] ||= action + options[:as] = name_for_action(action, options[:as]) + + with_exclusive_scope do + return super(path, options) + end + elsif resource_method_scope? + path = path_for_custom_action + options[:as] = name_for_action(options[:as]) if options[:as] + args.push(options) with_exclusive_scope do - return match(path, options) + scope(path) do + return super + end end end @@ -736,8 +778,8 @@ module ActionDispatch def root(options={}) if @scope[:scope_level] == :resources - with_scope_level(:collection) do - scope(parent_resource.path, :name_prefix => parent_resource.collection_name) do + with_scope_level(:nested) do + scope(parent_resource.path, :as => parent_resource.collection_name) do super(options) end end @@ -747,11 +789,11 @@ module ActionDispatch end protected + def parent_resource #:nodoc: @scope[:scope_level_resource] end - private def apply_common_behavior_for(method, resources, options, &block) if resources.length > 1 resources.each { |r| send(method, r, options, &block) } @@ -765,6 +807,10 @@ module ActionDispatch return true end + scope_options = @scope.slice(*MERGE_FROM_SCOPE_OPTIONS).delete_if{ |k,v| v.blank? } + options.reverse_merge!(scope_options) unless scope_options.empty? + options.reverse_merge!(@scope[:options]) unless @scope[:options].blank? + if resource_scope? nested do send(method, resources.pop, options, &block) @@ -779,14 +825,20 @@ module ActionDispatch [:resource, :resources].include?(@scope[:scope_level]) end + def resource_method_scope? + [:collection, :member, :new].include?(@scope[:scope_level]) + end + def with_exclusive_scope begin - old_name_prefix, old_path = @scope[:name_prefix], @scope[:path] - @scope[:name_prefix], @scope[:path] = nil, nil + old_name_prefix, old_path = @scope[:as], @scope[:path] + @scope[:as], @scope[:path] = nil, nil - yield + with_scope_level(:exclusive) do + yield + end ensure - @scope[:name_prefix], @scope[:path] = old_name_prefix, old_path + @scope[:as], @scope[:path] = old_name_prefix, old_path end end @@ -807,6 +859,14 @@ module ActionDispatch end end + def new_scope + with_scope_level(:new) do + scope(*parent_resource.new_scope(action_path(:new))) do + yield + end + end + end + def collection_scope with_scope_level(:collection) do scope(*parent_resource.collection_scope) do @@ -823,91 +883,71 @@ module ActionDispatch end end - def path_for_action(action, path_names) - case action - when :index, :create - "#{@scope[:path]}(.:format)" - when :show, :update, :destroy - if parent_resource.shallow? - "#{@scope[:module]}/#{parent_resource.path}/:id(.:format)" - else - "#{@scope[:path]}(.:format)" - end - when :new - "#{@scope[:path]}/#{action_path(:new)}(.:format)" - when :edit - if parent_resource.shallow? - "#{@scope[:module]}/#{parent_resource.path}/:id/#{action_path(:edit)}(.:format)" - else - "#{@scope[:path]}/#{action_path(:edit)}(.:format)" - end + def canonical_action?(action, flag) + flag && CANONICAL_ACTIONS.include?(action) + end + + def shallow_scoping? + parent_resource && parent_resource.shallow? && @scope[:scope_level] == :member + end + + def path_for_action(action, path) + prefix = shallow_scoping? ? + "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path] + + if canonical_action?(action, path.blank?) + "#{prefix}(.:format)" else - case @scope[:scope_level] - when :collection - "#{@scope[:path]}/#{action_path(action)}(.:format)" - when :new - "#{@scope[:path]}/#{action_path(:new)}/#{action_path(action)}(.:format)" - else - if parent_resource.shallow? - "#{@scope[:module]}/#{parent_resource.path}/:id/#{action_path(action)}(.:format)" - else - "#{@scope[:path]}/#{action_path(action)}(.:format)" - end - end + "#{prefix}/#{action_path(action, path)}(.:format)" end end - def action_path(name, path_names = nil) - path_names ||= @scope[:path_names] - path_names[name.to_sym] || name.to_s + def path_for_custom_action + if shallow_scoping? + "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" + else + @scope[:path] + end end - def options_for_action(action, options) - options.reverse_merge( - :to => action, - :as => name_for_action(action) - ) + def action_path(name, path = nil) + path || @scope[:path_names][name.to_sym] || name.to_s + end + + def prefix_name_for_action(action, as) + if as.present? + "#{as}_" + elsif as + "" + elsif !canonical_action?(action, @scope[:scope_level]) + "#{action}_" + end end - def name_for_action(action) - name_prefix = @scope[:name_prefix].blank? ? "" : "#{@scope[:name_prefix]}_" - shallow_prefix = @scope[:module].blank? ? "" : "#{@scope[:module].tr('/', '_')}_" + def name_for_action(action, as=nil) + prefix = prefix_name_for_action(action, as) + name_prefix = @scope[:as] - case action - when :index - "#{name_prefix}#{parent_resource.collection_name}" - when :show - if parent_resource.shallow? - "#{shallow_prefix}#{parent_resource.member_name}" - else - "#{name_prefix}#{parent_resource.member_name}" - end - when :edit - if parent_resource.shallow? - "edit_#{shallow_prefix}#{parent_resource.member_name}" - else - "edit_#{name_prefix}#{parent_resource.member_name}" - end + if parent_resource + collection_name = parent_resource.collection_name + member_name = parent_resource.member_name + name_prefix = "#{name_prefix}_" if name_prefix.present? + end + + case @scope[:scope_level] + when :collection + "#{prefix}#{name_prefix}#{collection_name}" when :new - "new_#{name_prefix}#{parent_resource.member_name}" - when :update, :create, :destroy - nil + "#{prefix}new_#{name_prefix}#{member_name}" else - case @scope[:scope_level] - when :collection - "#{action}_#{name_prefix}#{parent_resource.collection_name}" - when :new - "#{action}_new_#{name_prefix}#{parent_resource.member_name}" + if shallow_scoping? + shallow_prefix = "#{@scope[:shallow_prefix]}_" if @scope[:shallow_prefix].present? + "#{prefix}#{shallow_prefix}#{member_name}" else - if parent_resource.shallow? - "#{action}_#{shallow_prefix}#{parent_resource.member_name}" - else - "#{action}_#{name_prefix}#{parent_resource.member_name}" - end + "#{prefix}#{name_prefix}#{member_name}" end end end - end include Base diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb index 6f37eadd6b..aefebf8f80 100644 --- a/actionpack/lib/action_dispatch/routing/route.rb +++ b/actionpack/lib/action_dispatch/routing/route.rb @@ -2,9 +2,10 @@ module ActionDispatch module Routing class Route #:nodoc: attr_reader :app, :conditions, :defaults, :name - attr_reader :path, :requirements + attr_reader :path, :requirements, :set - def initialize(app, conditions, requirements, defaults, name, anchor) + def initialize(set, app, conditions, requirements, defaults, name, anchor) + @set = set @app = app @defaults = defaults @name = name @@ -24,6 +25,9 @@ module ActionDispatch h[k] = Rack::Mount::RegexpWithNamedGroups.new(v) h } + + @conditions.delete_if{ |k,v| k != :path_info && !valid_condition?(k) } + @requirements.delete_if{ |k,v| !valid_condition?(k) } end def verb @@ -50,6 +54,11 @@ module ActionDispatch "%-6s %-40s %s" % [(verb || :any).to_s.upcase, path, requirements.inspect] end end + + private + def valid_condition?(method) + segment_keys.include?(method) || set.valid_conditions.include?(method) + end end end end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 57a73dde75..5ecad6bc04 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,8 +1,10 @@ -require 'rack/mount' require 'forwardable' require 'active_support/core_ext/object/to_query' require 'action_dispatch/routing/deprecated_mapper' +$: << File.expand_path('../../vendor/rack-mount-0.6.6.pre', __FILE__) +require 'rack/mount' + module ActionDispatch module Routing class RouteSet #:nodoc: @@ -185,9 +187,9 @@ module ActionDispatch end end - attr_accessor :routes, :named_routes + attr_accessor :set, :routes, :named_routes attr_accessor :disable_clear_and_finalize, :resources_path_names - attr_accessor :default_url_options, :request_class + attr_accessor :default_url_options, :request_class, :valid_conditions def self.default_resources_path_names { :new => 'new', :edit => 'edit' } @@ -199,7 +201,11 @@ module ActionDispatch self.resources_path_names = self.class.default_resources_path_names.dup self.controller_namespaces = Set.new self.default_url_options = {} + self.request_class = request_class + self.valid_conditions = request_class.public_instance_methods.map { |m| m.to_sym } + self.valid_conditions.delete(:id) + self.valid_conditions.push(:controller, :action) @disable_clear_and_finalize = false clear! @@ -277,7 +283,7 @@ module ActionDispatch end def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) - route = Route.new(app, conditions, requirements, defaults, name, anchor) + route = Route.new(self, app, conditions, requirements, defaults, name, anchor) @set.add_route(*route) named_routes[name] = route if name routes << route @@ -296,6 +302,7 @@ module ActionDispatch @extras = extras normalize_options! + normalize_recall! normalize_controller_action_id! use_relative_controller! controller.sub!(%r{^/}, '') if controller @@ -336,6 +343,15 @@ module ActionDispatch end end + def normalize_recall! + # If the target route is not a standard route then remove controller and action + # from the options otherwise they will appear in the url parameters + if block_or_proc_route_target? + recall.delete(:controller) unless segment_keys.include?(:controller) + recall.delete(:action) unless segment_keys.include?(:action) + end + end + # This pulls :controller, :action, and :id out of the recall. # The recall key is only used if there is no key in the options # or if the key in the options is identical. If any of @@ -371,7 +387,7 @@ module ActionDispatch def generate error = ActionController::RoutingError.new("No route matches #{options.inspect}") - path, params = @set.generate(:path_info, named_route, options, recall, opts) + path, params = @set.set.generate(:path_info, named_route, options, recall, opts) raise error unless path @@ -402,6 +418,19 @@ module ActionDispatch return false unless current_controller controller.to_param != current_controller.to_param end + + private + def named_route_exists? + named_route && set.named_routes[named_route] + end + + def block_or_proc_route_target? + named_route_exists? && !set.named_routes[named_route].app.is_a?(Dispatcher) + end + + def segment_keys + named_route_exists? ? set.named_routes[named_route].segment_keys : [] + end end # Generate the path indicated by the arguments, and return an array of @@ -415,7 +444,7 @@ module ActionDispatch end def generate(options, recall = {}, extras = false) - Generator.new(options, recall, @set, extras).generate + Generator.new(options, recall, self, extras).generate end RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash] @@ -447,7 +476,7 @@ module ActionDispatch # ROUTES TODO: This can be called directly, so script_name should probably be set in the router rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) - rewritten_url << "##{Rack::Utils.escape(options[:anchor].to_param.to_s)}" if options[:anchor] + rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor] rewritten_url end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 1499c03bdf..9338fa9e70 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -53,7 +53,6 @@ module ActionDispatch extras.each_key { |key| expected_options.delete key } unless extras.nil? expected_options.stringify_keys! - routing_diff = expected_options.diff(request.path_parameters) msg = build_message(message, "The recognized options <?> did not match <?>, difference: <?>", request.path_parameters, expected_options, expected_options.diff(request.path_parameters)) assert_block(msg) { request.path_parameters == expected_options } diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 0e82b41590..2fc9e2b7d6 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -7,9 +7,7 @@ require 'action_controller/vendor/html-scanner' module ActionDispatch module Assertions - unless const_defined?(:NO_STRIP) - NO_STRIP = %w{pre script style textarea} - end + NO_STRIP = %w{pre script style textarea} # Adds the +assert_select+ method for use in Rails functional # test cases, which can be used to make assertions on the response HTML of a controller @@ -359,7 +357,7 @@ module ActionDispatch # position. Possible values are <tt>:top</tt>, <tt>:bottom</tt>, <tt>:before</tt> # and <tt>:after</tt>. # - # Use the argument <tt>:redirect</tt> follwed by a path to check that an statement + # Use the argument <tt>:redirect</tt> followed by a path to check that an statement # which redirects to the specified path is generated. # # Using the <tt>:remove</tt> statement, you will be able to pass a block, but it will @@ -581,27 +579,25 @@ module ActionDispatch end protected - unless const_defined?(:RJS_STATEMENTS) - RJS_PATTERN_HTML = "\"((\\\\\"|[^\"])*)\"" - RJS_ANY_ID = "\"([^\"])*\"" - RJS_STATEMENTS = { - :chained_replace => "\\$\\(#{RJS_ANY_ID}\\)\\.replace\\(#{RJS_PATTERN_HTML}\\)", - :chained_replace_html => "\\$\\(#{RJS_ANY_ID}\\)\\.update\\(#{RJS_PATTERN_HTML}\\)", - :replace_html => "Element\\.update\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)", - :replace => "Element\\.replace\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)", - :redirect => "window.location.href = #{RJS_ANY_ID}" - } - [:remove, :show, :hide, :toggle].each do |action| - RJS_STATEMENTS[action] = "Element\\.#{action}\\(#{RJS_ANY_ID}\\)" - end - RJS_INSERTIONS = ["top", "bottom", "before", "after"] - RJS_INSERTIONS.each do |insertion| - RJS_STATEMENTS["insert_#{insertion}".to_sym] = "Element.insert\\(#{RJS_ANY_ID}, \\{ #{insertion}: #{RJS_PATTERN_HTML} \\}\\)" - end - RJS_STATEMENTS[:insert_html] = "Element.insert\\(#{RJS_ANY_ID}, \\{ (#{RJS_INSERTIONS.join('|')}): #{RJS_PATTERN_HTML} \\}\\)" - RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") - RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/ + RJS_PATTERN_HTML = "\"((\\\\\"|[^\"])*)\"" + RJS_ANY_ID = "\"([^\"])*\"" + RJS_STATEMENTS = { + :chained_replace => "\\$\\(#{RJS_ANY_ID}\\)\\.replace\\(#{RJS_PATTERN_HTML}\\)", + :chained_replace_html => "\\$\\(#{RJS_ANY_ID}\\)\\.update\\(#{RJS_PATTERN_HTML}\\)", + :replace_html => "Element\\.update\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)", + :replace => "Element\\.replace\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)", + :redirect => "window.location.href = #{RJS_ANY_ID}" + } + [:remove, :show, :hide, :toggle].each do |action| + RJS_STATEMENTS[action] = "Element\\.#{action}\\(#{RJS_ANY_ID}\\)" + end + RJS_INSERTIONS = ["top", "bottom", "before", "after"] + RJS_INSERTIONS.each do |insertion| + RJS_STATEMENTS["insert_#{insertion}".to_sym] = "Element.insert\\(#{RJS_ANY_ID}, \\{ #{insertion}: #{RJS_PATTERN_HTML} \\}\\)" end + RJS_STATEMENTS[:insert_html] = "Element.insert\\(#{RJS_ANY_ID}, \\{ (#{RJS_INSERTIONS.join('|')}): #{RJS_PATTERN_HTML} \\}\\)" + RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") + RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/ # +assert_select+ and +css_select+ call this to obtain the content in the HTML # page, or from all the RJS statements, depending on the type of response. diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index 9a51a32899..44fb1bde99 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -53,7 +53,7 @@ module ActionDispatch # Returns the template of the file which was used to # render this response (or nil) def rendered - ActiveSupport::Deprecation.warn("response.rendered has been deprecated. Use tempate.rendered instead", caller) + ActiveSupport::Deprecation.warn("response.rendered has been deprecated. Use template.rendered instead", caller) @template.instance_variable_get(:@_rendered) end @@ -89,7 +89,7 @@ module ActionDispatch # A shortcut to the template.assigns def template_objects - ActiveSupport::Deprecation.warn("response.template_objects has been deprecated. Use tempate.assigns instead", caller) + ActiveSupport::Deprecation.warn("response.template_objects has been deprecated. Use template.assigns instead", caller) @template.assigns || {} end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount.rb new file mode 100644 index 0000000000..9fbf707724 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount.rb @@ -0,0 +1,32 @@ +require 'rack' + +module Rack #:nodoc: + # A stackable dynamic tree based Rack router. + # + # Rack::Mount supports Rack's Cascade style of trying several routes until + # it finds one that is not a 404. This allows multiple routes to be nested + # or stacked on top of each other. Since the application endpoint can + # trigger the router to continue matching, middleware can be used to add + # arbitrary conditions to any route. This allows you to route based on + # other request attributes, session information, or even data dynamically + # pulled from a database. + module Mount + autoload :CodeGeneration, 'rack/mount/code_generation' + autoload :GeneratableRegexp, 'rack/mount/generatable_regexp' + autoload :Multimap, 'rack/mount/multimap' + autoload :Prefix, 'rack/mount/prefix' + autoload :RegexpWithNamedGroups, 'rack/mount/regexp_with_named_groups' + autoload :Route, 'rack/mount/route' + autoload :RouteSet, 'rack/mount/route_set' + autoload :RoutingError, 'rack/mount/route_set' + autoload :Strexp, 'rack/mount/strexp' + autoload :Utils, 'rack/mount/utils' + autoload :Version, 'rack/mount/version' + + module Analysis #:nodoc: + autoload :Frequency, 'rack/mount/analysis/frequency' + autoload :Histogram, 'rack/mount/analysis/histogram' + autoload :Splitting, 'rack/mount/analysis/splitting' + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/frequency.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/frequency.rb new file mode 100644 index 0000000000..671258f807 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/frequency.rb @@ -0,0 +1,60 @@ +require 'rack/mount/utils' + +module Rack::Mount + module Analysis + class Frequency #:nodoc: + def initialize(*keys) + clear + keys.each { |key| self << key } + end + + def clear + @raw_keys = [] + @key_frequency = Analysis::Histogram.new + self + end + + def <<(key) + raise ArgumentError unless key.is_a?(Hash) + @raw_keys << key + nil + end + + def possible_keys + @possible_keys ||= begin + @raw_keys.map do |key| + key.inject({}) { |requirements, (method, requirement)| + process_key(requirements, method, requirement) + requirements + } + end + end + end + + def process_key(requirements, method, requirement) + if requirement.is_a?(Regexp) + expression = Utils.parse_regexp(requirement) + + if expression.is_a?(Regin::Expression) && expression.anchored_to_line? + expression = Regin::Expression.new(expression.reject { |e| e.is_a?(Regin::Anchor) }) + return requirements[method] = expression.to_s if expression.literal? + end + end + + requirements[method] = requirement + end + + def report + @report ||= begin + possible_keys.each { |keys| keys.each_pair { |key, _| @key_frequency << key } } + return [] if @key_frequency.count <= 1 + @key_frequency.keys_in_upper_quartile + end + end + + def expire! + @possible_keys = @report = nil + end + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/histogram.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/histogram.rb new file mode 100644 index 0000000000..20aaa132f9 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/histogram.rb @@ -0,0 +1,74 @@ +module Rack::Mount + module Analysis + class Histogram < Hash #:nodoc: + attr_reader :count + + def initialize + @count = 0 + super(0) + expire_caches! + end + + def <<(value) + @count += 1 + self[value] += 1 if value + expire_caches! + self + end + + def sorted_by_frequency + sort_by { |_, value| value }.reverse! + end + + def max + @max ||= values.max || 0 + end + + def min + @min ||= values.min || 0 + end + + def mean + @mean ||= calculate_mean + end + + def standard_deviation + @standard_deviation ||= calculate_standard_deviation + end + + def upper_quartile_limit + @upper_quartile_limit ||= calculate_upper_quartile_limit + end + + def keys_in_upper_quartile + @keys_in_upper_quartile ||= compute_keys_in_upper_quartile + end + + private + def calculate_mean + count / size + end + + def calculate_variance + values.inject(0) { |sum, e| sum + (e - mean) ** 2 } / count.to_f + end + + def calculate_standard_deviation + Math.sqrt(calculate_variance) + end + + def calculate_upper_quartile_limit + mean + standard_deviation + end + + def compute_keys_in_upper_quartile + sorted_by_frequency.select { |_, value| value >= upper_quartile_limit }.map! { |key, _| key } + end + + def expire_caches! + @max = @min = @mean = @standard_deviation = nil + @keys_in_upper_quartile = nil + end + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/splitting.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/splitting.rb new file mode 100644 index 0000000000..8a8c551302 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/splitting.rb @@ -0,0 +1,159 @@ +require 'rack/mount/utils' + +module Rack::Mount + module Analysis + class Splitting < Frequency + NULL = "\0".freeze + + class Key < Struct.new(:method, :index, :separators) + def self.split(value, separator_pattern) + keys = value.split(separator_pattern) + keys.shift if keys[0] == '' + keys << NULL + keys + end + + def call(cache, obj) + (cache[method] ||= self.class.split(obj.send(method), separators))[index] + end + + def call_source(cache, obj) + "(#{cache}[:#{method}] ||= Analysis::Splitting::Key.split(#{obj}.#{method}, #{separators.inspect}))[#{index}]" + end + + def inspect + "#{method}[#{index}]" + end + end + + def clear + @boundaries = {} + super + end + + def <<(key) + super + key.each_pair do |k, v| + analyze_capture_boundaries(v, @boundaries[k] ||= Histogram.new) + end + end + + def separators(key) + (@boundaries[key].keys_in_upper_quartile + ['/']).uniq + end + + def process_key(requirements, method, requirement) + separators = separators(method) + if requirement.is_a?(Regexp) && separators.any? + generate_split_keys(requirement, separators).each_with_index do |value, index| + requirements[Key.new(method, index, Regexp.union(*separators))] = value + end + else + super + end + end + + private + def analyze_capture_boundaries(regexp, boundaries) #:nodoc: + return boundaries unless regexp.is_a?(Regexp) + + parts = Utils.parse_regexp(regexp) + parts.each_with_index do |part, index| + if part.is_a?(Regin::Group) + if index > 0 + previous = parts[index-1] + if previous.is_a?(Regin::Character) && previous.literal? + boundaries << previous.to_s + end + end + + if inside = part.expression[0] + if inside.is_a?(Regin::Character) && inside.literal? + boundaries << inside.to_s + end + end + + if index < parts.length + following = parts[index+1] + if following.is_a?(Regin::Character) && following.literal? + boundaries << following.to_s + end + end + end + end + + boundaries + end + + def generate_split_keys(regexp, separators) #:nodoc: + segments = [] + buf = nil + parts = Utils.parse_regexp(regexp) + parts.each_with_index do |part, index| + case part + when Regin::Anchor + if part.value == '$' || part.value == '\Z' + segments << join_buffer(buf, regexp) if buf + segments << NULL + buf = nil + break + end + when Regin::CharacterClass + break if separators.any? { |s| part.include?(s) } + buf = nil + segments << part.to_regexp(true) + when Regin::Character + if separators.any? { |s| part.include?(s) } + segments << join_buffer(buf, regexp) if buf + peek = parts[index+1] + if peek.is_a?(Regin::Character) && separators.include?(peek.value) + segments << '' + end + buf = nil + else + buf ||= Regin::Expression.new([]) + buf += [part] + end + when Regin::Group + if part.quantifier == '?' + value = part.expression.first + if separators.any? { |s| value.include?(s) } + segments << join_buffer(buf, regexp) if buf + buf = nil + end + break + elsif part.quantifier == nil + break if separators.any? { |s| part.include?(s) } + buf = nil + segments << part.to_regexp(true) + else + break + end + else + break + end + + if index + 1 == parts.size + segments << join_buffer(buf, regexp) if buf + buf = nil + break + end + end + + while segments.length > 0 && (segments.last.nil? || segments.last == '') + segments.pop + end + + segments + end + + def join_buffer(parts, regexp) + if parts.literal? + parts.to_s + else + parts.to_regexp(true) + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/code_generation.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/code_generation.rb new file mode 100644 index 0000000000..903c79fdc6 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/code_generation.rb @@ -0,0 +1,113 @@ +module Rack::Mount + module CodeGeneration #:nodoc: + def _expired_recognize(env) #:nodoc: + raise 'route set not finalized' + end + + def rehash + super + optimize_recognize! + end + + private + def expire! + if @optimized_recognize_defined + remove_metaclass_method :recognize + + class << self + alias_method :recognize, :_expired_recognize + end + + @optimized_recognize_defined = false + end + + super + end + + def optimize_container_iterator(container) + body = [] + + container.each_with_index { |route, i| + body << "route = self[#{i}]" + body << 'matches = {}' + body << 'params = route.defaults.dup' + + conditions = [] + route.conditions.each do |method, condition| + b = [] + if condition.is_a?(Regexp) + b << "if m = obj.#{method}.match(#{condition.inspect})" + b << "matches[:#{method}] = m" + if (named_captures = route.named_captures[method]) && named_captures.any? + b << 'captures = m.captures' + b << 'p = nil' + b << named_captures.map { |k, j| "params[#{k.inspect}] = p if p = captures[#{j}]" }.join('; ') + end + else + b << "if m = obj.#{method} == route.conditions[:#{method}]" + end + b << 'true' + b << 'end' + conditions << "(#{b.join('; ')})" + end + + body << <<-RUBY + if #{conditions.join(' && ')} + yield route, matches, params + end + RUBY + } + + container.instance_eval(<<-RUBY, __FILE__, __LINE__) + def optimized_each(obj) + #{body.join("\n")} + nil + end + RUBY + end + + def optimize_recognize! + keys = @recognition_keys.map { |key| + if key.respond_to?(:call_source) + key.call_source(:cache, :obj) + else + "obj.#{key}" + end + }.join(', ') + + @optimized_recognize_defined = true + + remove_metaclass_method :recognize + + instance_eval(<<-RUBY, __FILE__, __LINE__) + def recognize(obj) + cache = {} + container = @recognition_graph[#{keys}] + optimize_container_iterator(container) unless container.respond_to?(:optimized_each) + + if block_given? + container.optimized_each(obj) do |route, matches, params| + yield route, matches, params + end + else + container.optimized_each(obj) do |route, matches, params| + return route, matches, params + end + end + + nil + end + RUBY + end + + # method_defined? can't distinguish between instance + # and meta methods. So we have to rescue if the method + # has not been defined in the metaclass yet. + def remove_metaclass_method(symbol) + metaclass = class << self; self; end + metaclass.send(:remove_method, symbol) + rescue NameError => e + nil + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/generatable_regexp.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/generatable_regexp.rb new file mode 100644 index 0000000000..47bbab3784 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/generatable_regexp.rb @@ -0,0 +1,210 @@ +require 'rack/mount/utils' + +module Rack::Mount + class GeneratableRegexp < Regexp #:nodoc: + class DynamicSegment #:nodoc: + attr_reader :name, :requirement + + def initialize(name, requirement) + @name, @requirement = name.to_sym, requirement + freeze + end + + def ==(obj) + @name == obj.name && @requirement == obj.requirement + end + + def =~(str) + @requirement =~ str + end + + def to_hash + { @name => @requirement } + end + + def inspect + "/(?<#{@name}>#{@requirement.source})/" + end + end + + module InstanceMethods + def self.extended(obj) + obj.segments + end + + def defaults=(defaults) + @required_captures = nil + @required_params = nil + @required_defaults = nil + @defaults = defaults + end + + def defaults + @defaults ||= {} + end + + def generatable? + segments.any? + end + + def generate(params = {}, recall = {}, options = {}) + return nil unless generatable? + + merged = recall.merge(params) + return nil unless required_params.all? { |p| merged.include?(p) } + return nil unless required_defaults.all? { |k, v| merged[k] == v } + + generate_from_segments(segments, params, merged, options) + end + + def segments + @segments ||= begin + defaults + segments = [] + catch(:halt) do + expression = Utils.parse_regexp(self) + segments = parse_segments(expression) + end + segments + end + end + + def captures + segments.flatten.find_all { |s| s.is_a?(DynamicSegment) } + end + + def required_captures + @required_captures ||= segments.find_all { |s| + s.is_a?(DynamicSegment) && !@defaults.include?(s.name) + }.freeze + end + + def required_params + @required_params ||= required_captures.map { |s| s.name }.freeze + end + + def required_defaults + @required_defaults ||= begin + required_defaults = @defaults.dup + captures.inject({}) { |h, s| h.merge!(s.to_hash) }.keys.each { |name| + required_defaults.delete(name) + } + required_defaults + end + end + + def freeze + segments + captures + required_captures + required_params + required_defaults + super + end + + private + def parse_segments(segments) + s = [] + segments.each_with_index do |part, index| + case part + when Regin::Anchor + # ignore + when Regin::Character + throw :halt unless part.literal? + + if s.last.is_a?(String) + s.last << part.value.dup + else + s << part.value.dup + end + when Regin::Group + if part.name + s << DynamicSegment.new(part.name, part.expression.to_regexp(true)) + else + s << parse_segments(part.expression) + end + when Regin::Expression + return parse_segments(part) + else + throw :halt + end + end + + s + end + + EMPTY_STRING = ''.freeze + + def generate_from_segments(segments, params, merged, options, optional = false) + if optional + return EMPTY_STRING if segments.all? { |s| s.is_a?(String) } + return EMPTY_STRING unless segments.flatten.any? { |s| + params.has_key?(s.name) if s.is_a?(DynamicSegment) + } + return EMPTY_STRING if segments.any? { |segment| + if segment.is_a?(DynamicSegment) + value = merged[segment.name] || @defaults[segment.name] + value = parameterize(segment.name, value, options) + + merged_value = parameterize(segment.name, merged[segment.name], options) + default_value = parameterize(segment.name, @defaults[segment.name], options) + + if value.nil? || segment !~ value + true + elsif merged_value == default_value + # Nasty control flow + return :clear_remaining_segments + else + false + end + end + } + end + + generated = segments.map do |segment| + case segment + when String + segment + when DynamicSegment + value = params[segment.name] || merged[segment.name] || @defaults[segment.name] + value = parameterize(segment.name, value, options) + if value && segment =~ value.to_s + value + else + return + end + when Array + value = generate_from_segments(segment, params, merged, options, true) + if value == :clear_remaining_segments + segment.each { |s| params.delete(s.name) if s.is_a?(DynamicSegment) } + EMPTY_STRING + elsif value.nil? + EMPTY_STRING + else + value + end + end + end + + # Delete any used items from the params + segments.each { |s| params.delete(s.name) if s.is_a?(DynamicSegment) } + + generated.join + end + + def parameterize(name, value, options) + if block = options[:parameterize] + block.call(name, value) + else + value + end + end + end + include InstanceMethods + + def initialize(regexp) + super + segments + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/multimap.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/multimap.rb new file mode 100644 index 0000000000..0f8eaaec67 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/multimap.rb @@ -0,0 +1,53 @@ +begin + require 'nested_multimap' +rescue LoadError + $: << File.expand_path(File.join(File.dirname(__FILE__), 'vendor/multimap')) + require 'nested_multimap' +end + +module Rack::Mount + class Multimap < NestedMultimap #:nodoc: + def store(*args) + keys = args.dup + value = keys.pop + key = keys.shift + + raise ArgumentError, 'wrong number of arguments (1 for 2)' unless value + + unless key.respond_to?(:=~) + raise ArgumentError, "unsupported key: #{args.first.inspect}" + end + + if key.is_a?(Regexp) + if keys.empty? + @hash.each_pair { |k, l| l << value if k =~ key } + self.default << value + else + @hash.each_pair { |k, _| + if k =~ key + args[0] = k + super(*args) + end + } + + self.default = self.class.new(default) unless default.is_a?(self.class) + default[*keys.dup] = value + end + else + super(*args) + end + end + alias_method :[]=, :store + + undef :index, :invert + + def height + containers_with_default.max { |a, b| a.length <=> b.length }.length + end + + def average_height + lengths = containers_with_default.map { |e| e.length } + lengths.inject(0) { |sum, len| sum += len }.to_f / lengths.size + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/prefix.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/prefix.rb new file mode 100644 index 0000000000..58892e4c4f --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/prefix.rb @@ -0,0 +1,36 @@ +require 'rack/mount/utils' + +module Rack::Mount + class Prefix #:nodoc: + EMPTY_STRING = ''.freeze + PATH_INFO = 'PATH_INFO'.freeze + SCRIPT_NAME = 'SCRIPT_NAME'.freeze + SLASH = '/'.freeze + + KEY = 'rack.mount.prefix'.freeze + + def initialize(app, prefix = nil) + @app, @prefix = app, prefix.freeze + freeze + end + + def call(env) + if prefix = env[KEY] || @prefix + old_path_info = env[PATH_INFO].dup + old_script_name = env[SCRIPT_NAME].dup + + begin + env[PATH_INFO] = Utils.normalize_path(env[PATH_INFO].sub(prefix, EMPTY_STRING)) + env[PATH_INFO] = EMPTY_STRING if env[PATH_INFO] == SLASH + env[SCRIPT_NAME] = Utils.normalize_path(env[SCRIPT_NAME].to_s + prefix) + @app.call(env) + ensure + env[PATH_INFO] = old_path_info + env[SCRIPT_NAME] = old_script_name + end + else + @app.call(env) + end + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/regexp_with_named_groups.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/regexp_with_named_groups.rb new file mode 100644 index 0000000000..c11292b2a2 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/regexp_with_named_groups.rb @@ -0,0 +1,69 @@ +module Rack::Mount + if Regin.regexp_supports_named_captures? + RegexpWithNamedGroups = Regexp + else + require 'strscan' + + # A wrapper that adds shim named capture support to older + # versions of Ruby. + # + # Because the named capture syntax causes a parse error, an + # alternate syntax is used to indicate named captures. + # + # Ruby 1.9+ named capture syntax: + # + # /(?<foo>[a-z]+)/ + # + # Ruby 1.8 shim syntax: + # + # /(?:<foo>[a-z]+)/ + class RegexpWithNamedGroups < Regexp + def self.new(regexp) #:nodoc: + if regexp.is_a?(RegexpWithNamedGroups) + regexp + else + super + end + end + + # Wraps Regexp with named capture support. + def initialize(regexp) + regexp = Regexp.compile(regexp) unless regexp.is_a?(Regexp) + source, options = regexp.source, regexp.options + @names, scanner = [], StringScanner.new(source) + + while scanner.skip_until(/\(/) + if scanner.scan(/\?:<([^>]+)>/) + @names << scanner[1] + elsif scanner.scan(/\?(i?m?x?\-?i?m?x?)?:/) + # ignore noncapture + else + @names << nil + end + end + source.gsub!(/\?:<([^>]+)>/, '') + + @names = [] unless @names.any? + @names.freeze + + super(source, options) + end + + def names + @names.dup + end + + def named_captures + named_captures = {} + names.each_with_index { |n, i| + named_captures[n] = [i+1] if n + } + named_captures + end + + def eql?(other) + super && @names.eql?(other.names) + end + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route.rb new file mode 100644 index 0000000000..680c40f147 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route.rb @@ -0,0 +1,130 @@ +require 'rack/mount/generatable_regexp' +require 'rack/mount/regexp_with_named_groups' +require 'rack/mount/utils' + +module Rack::Mount + # Route is an internal class used to wrap a single route attributes. + # + # Plugins should not depend on any method on this class or instantiate + # new Route objects. Instead use the factory method, RouteSet#add_route + # to create new routes and add them to the set. + class Route + # Valid rack application to call if conditions are met + attr_reader :app + + # A hash of conditions to match against. Conditions may be expressed + # as strings or regexps to match against. + attr_reader :conditions + + # A hash of values that always gets merged into the parameters hash + attr_reader :defaults + + # Symbol identifier for the route used with named route generations + attr_reader :name + + attr_reader :named_captures + + def initialize(app, conditions, defaults, name) + unless app.respond_to?(:call) + raise ArgumentError, 'app must be a valid rack application' \ + ' and respond to call' + end + @app = app + + @name = name ? name.to_sym : nil + @defaults = (defaults || {}).freeze + + @conditions = {} + + conditions.each do |method, pattern| + next unless method && pattern + + pattern = Regexp.compile("\\A#{Regexp.escape(pattern)}\\Z") if pattern.is_a?(String) + + if pattern.is_a?(Regexp) + pattern = Utils.normalize_extended_expression(pattern) + pattern = RegexpWithNamedGroups.new(pattern) + pattern.extend(GeneratableRegexp::InstanceMethods) + pattern.defaults = @defaults + end + + @conditions[method] = pattern.freeze + end + + @named_captures = {} + @conditions.map { |method, condition| + next unless condition.respond_to?(:named_captures) + @named_captures[method] = condition.named_captures.inject({}) { |named_captures, (k, v)| + named_captures[k.to_sym] = v.last - 1 + named_captures + }.freeze + } + @named_captures.freeze + + @has_significant_params = @conditions.any? { |method, condition| + (condition.respond_to?(:required_params) && condition.required_params.any?) || + (condition.respond_to?(:required_defaults) && condition.required_defaults.any?) + } + + if @conditions.has_key?(:path_info) && + !Utils.regexp_anchored?(@conditions[:path_info]) + @prefix = true + @app = Prefix.new(@app) + else + @prefix = false + end + + @conditions.freeze + end + + def prefix? + @prefix + end + + + def generation_keys + @conditions.inject({}) { |keys, (method, condition)| + if condition.respond_to?(:required_defaults) + keys.merge!(condition.required_defaults) + else + keys + end + } + end + + def significant_params? + @has_significant_params + end + + def generate(method, params = {}, recall = {}, options = {}) + if method.nil? + result = @conditions.inject({}) { |h, (m, condition)| + if condition.respond_to?(:generate) + h[m] = condition.generate(params, recall, options) + end + h + } + return nil if result.values.compact.empty? + else + if condition = @conditions[method] + if condition.respond_to?(:generate) + result = condition.generate(params, recall, options) + end + end + end + + if result + @defaults.each do |key, value| + params.delete(key) if params[key] == value + end + end + + result + end + + + def inspect #:nodoc: + "#<#{self.class.name} @app=#{@app.inspect} @conditions=#{@conditions.inspect} @defaults=#{@defaults.inspect} @name=#{@name.inspect}>" + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route_set.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route_set.rb new file mode 100644 index 0000000000..0e5a65a640 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route_set.rb @@ -0,0 +1,409 @@ +require 'rack/mount/multimap' +require 'rack/mount/route' +require 'rack/mount/utils' + +module Rack::Mount + class RoutingError < StandardError; end + + class RouteSet + # Initialize a new RouteSet without optimizations + def self.new_without_optimizations(options = {}, &block) + new(options.merge(:_optimize => false), &block) + end + + # Basic RouteSet initializer. + # + # If a block is given, the set is yielded and finalized. + # + # See other aspects for other valid options: + # - <tt>Generation::RouteSet.new</tt> + # - <tt>Recognition::RouteSet.new</tt> + def initialize(options = {}, &block) + @parameters_key = options.delete(:parameters_key) || 'rack.routing_args' + @parameters_key.freeze + + @named_routes = {} + + @recognition_key_analyzer = Analysis::Splitting.new + @generation_key_analyzer = Analysis::Frequency.new + + @request_class = options.delete(:request_class) || Rack::Request + @valid_conditions = @request_class.public_instance_methods.map! { |m| m.to_sym } + + extend CodeGeneration unless options[:_optimize] == false + @optimized_recognize_defined = false + + @routes = [] + expire! + + if block_given? + yield self + rehash + end + end + + # Builder method to add a route to the set + # + # <tt>app</tt>:: A valid Rack app to call if the conditions are met. + # <tt>conditions</tt>:: A hash of conditions to match against. + # Conditions may be expressed as strings or + # regexps to match against. + # <tt>defaults</tt>:: A hash of values that always gets merged in + # <tt>name</tt>:: Symbol identifier for the route used with named + # route generations + def add_route(app, conditions = {}, defaults = {}, name = nil) + unless conditions.is_a?(Hash) + raise ArgumentError, 'conditions must be a Hash' + end + + unless conditions.all? { |method, pattern| + @valid_conditions.include?(method) + } + raise ArgumentError, 'conditions may only include ' + + @valid_conditions.inspect + end + + route = Route.new(app, conditions, defaults, name) + @routes << route + + @recognition_key_analyzer << route.conditions + + @named_routes[route.name] = route if route.name + @generation_key_analyzer << route.generation_keys + + expire! + route + end + + def recognize(obj) + raise 'route set not finalized' unless @recognition_graph + + cache = {} + keys = @recognition_keys.map { |key| + if key.respond_to?(:call) + key.call(cache, obj) + else + obj.send(key) + end + } + + @recognition_graph[*keys].each do |route| + matches = {} + params = route.defaults.dup + + if route.conditions.all? { |method, condition| + value = obj.send(method) + if condition.is_a?(Regexp) && (m = value.match(condition)) + matches[method] = m + captures = m.captures + route.named_captures[method].each do |k, i| + if v = captures[i] + params[k] = v + end + end + true + elsif value == condition + true + else + false + end + } + if block_given? + yield route, matches, params + else + return route, matches, params + end + end + end + + nil + end + + X_CASCADE = 'X-Cascade'.freeze + PASS = 'pass'.freeze + PATH_INFO = 'PATH_INFO'.freeze + + # Rack compatible recognition and dispatching method. Routes are + # tried until one returns a non-catch status code. If no routes + # match, the catch status code is returned. + # + # This method can only be invoked after the RouteSet has been + # finalized. + def call(env) + raise 'route set not finalized' unless @recognition_graph + + env[PATH_INFO] = Utils.normalize_path(env[PATH_INFO]) + + request = nil + req = @request_class.new(env) + recognize(req) do |route, matches, params| + # TODO: We only want to unescape params from uri related methods + params.each { |k, v| params[k] = Utils.unescape_uri(v) if v.is_a?(String) } + + if route.prefix? + env[Prefix::KEY] = matches[:path_info].to_s + end + + env[@parameters_key] = params + result = route.app.call(env) + return result unless result[1][X_CASCADE] == PASS + end + + request || [404, {'Content-Type' => 'text/html', 'X-Cascade' => 'pass'}, ['Not Found']] + end + + # Generates a url from Rack env and identifiers or significant keys. + # + # To generate a url by named route, pass the name in as a +Symbol+. + # url(env, :dashboard) # => "/dashboard" + # + # Additional parameters can be passed in as a hash + # url(env, :people, :id => "1") # => "/people/1" + # + # If no name route is given, it will fall back to a slower + # generation search. + # url(env, :controller => "people", :action => "show", :id => "1") + # # => "/people/1" + def url(env, *args) + named_route, params = nil, {} + + case args.length + when 2 + named_route, params = args[0], args[1].dup + when 1 + if args[0].is_a?(Hash) + params = args[0].dup + else + named_route = args[0] + end + else + raise ArgumentError + end + + only_path = params.delete(:only_path) + recall = env[@parameters_key] || {} + + unless result = generate(:all, named_route, params, recall, + :parameterize => lambda { |name, param| Utils.escape_uri(param) }) + return + end + + parts, params = result + return unless parts + + params.each do |k, v| + if v + params[k] = v + else + params.delete(k) + end + end + + req = stubbed_request_class.new(env) + req._stubbed_values = parts.merge(:query_string => Utils.build_nested_query(params)) + only_path ? req.fullpath : req.url + end + + def generate(method, *args) #:nodoc: + raise 'route set not finalized' unless @generation_graph + + method = nil if method == :all + named_route, params, recall, options = extract_params!(*args) + merged = recall.merge(params) + route = nil + + if named_route + if route = @named_routes[named_route.to_sym] + recall = route.defaults.merge(recall) + url = route.generate(method, params, recall, options) + [url, params] + else + raise RoutingError, "#{named_route} failed to generate from #{params.inspect}" + end + else + keys = @generation_keys.map { |key| + if k = merged[key] + k.to_s + else + nil + end + } + @generation_graph[*keys].each do |r| + next unless r.significant_params? + if url = r.generate(method, params, recall, options) + return [url, params] + end + end + + raise RoutingError, "No route matches #{params.inspect}" + end + end + + # Number of routes in the set + def length + @routes.length + end + + def rehash #:nodoc: + @recognition_keys = build_recognition_keys + @recognition_graph = build_recognition_graph + @generation_keys = build_generation_keys + @generation_graph = build_generation_graph + end + + # Finalizes the set and builds optimized data structures. You *must* + # freeze the set before you can use <tt>call</tt> and <tt>url</tt>. + # So remember to call freeze after you are done adding routes. + def freeze + unless frozen? + rehash + + @recognition_key_analyzer = nil + @generation_key_analyzer = nil + @valid_conditions = nil + + @routes.each { |route| route.freeze } + @routes.freeze + end + + super + end + + def marshal_dump #:nodoc: + hash = {} + + instance_variables_to_serialize.each do |ivar| + hash[ivar] = instance_variable_get(ivar) + end + + if graph = hash[:@recognition_graph] + hash[:@recognition_graph] = graph.dup + end + + hash + end + + def marshal_load(hash) #:nodoc: + hash.each do |ivar, value| + instance_variable_set(ivar, value) + end + end + + protected + def recognition_stats + { :keys => @recognition_keys, + :keys_size => @recognition_keys.size, + :graph_size => @recognition_graph.size, + :graph_height => @recognition_graph.height, + :graph_average_height => @recognition_graph.average_height } + end + + private + def expire! #:nodoc: + @recognition_keys = @recognition_graph = nil + @recognition_key_analyzer.expire! + + @generation_keys = @generation_graph = nil + @generation_key_analyzer.expire! + end + + def instance_variables_to_serialize + instance_variables.map { |ivar| ivar.to_sym } - [:@stubbed_request_class, :@optimized_recognize_defined] + end + + # An internal helper method for constructing a nested set from + # the linear route set. + # + # build_nested_route_set([:request_method, :path_info]) { |route, method| + # route.send(method) + # } + def build_nested_route_set(keys, &block) + graph = Multimap.new + @routes.each_with_index do |route, index| + catch(:skip) do + k = keys.map { |key| block.call(key, index) } + Utils.pop_trailing_nils!(k) + k.map! { |key| key || /.+/ } + graph[*k] = route + end + end + graph + end + + def build_recognition_graph + build_nested_route_set(@recognition_keys) { |k, i| + @recognition_key_analyzer.possible_keys[i][k] + } + end + + def build_recognition_keys + @recognition_key_analyzer.report + end + + def build_generation_graph + build_nested_route_set(@generation_keys) { |k, i| + throw :skip unless @routes[i].significant_params? + + if k = @generation_key_analyzer.possible_keys[i][k] + k.to_s + else + nil + end + } + end + + def build_generation_keys + @generation_key_analyzer.report + end + + def extract_params!(*args) + case args.length + when 4 + named_route, params, recall, options = args + when 3 + if args[0].is_a?(Hash) + params, recall, options = args + else + named_route, params, recall = args + end + when 2 + if args[0].is_a?(Hash) + params, recall = args + else + named_route, params = args + end + when 1 + if args[0].is_a?(Hash) + params = args[0] + else + named_route = args[0] + end + else + raise ArgumentError + end + + named_route ||= nil + params ||= {} + recall ||= {} + options ||= {} + + [named_route, params.dup, recall.dup, options.dup] + end + + def stubbed_request_class + @stubbed_request_class ||= begin + klass = Class.new(@request_class) + klass.public_instance_methods.each do |method| + next if method =~ /^__|object_id/ + klass.class_eval <<-RUBY + def #{method}(*args, &block) + @_stubbed_values[:#{method}] || super + end + RUBY + end + klass.class_eval { attr_accessor :_stubbed_values } + klass + end + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp.rb new file mode 100644 index 0000000000..d0d8797008 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp.rb @@ -0,0 +1,68 @@ +require 'rack/mount/strexp/parser' + +module Rack::Mount + class Strexp + class << self + # Parses segmented string expression and converts it into a Regexp + # + # Strexp.compile('foo') + # # => %r{\Afoo\Z} + # + # Strexp.compile('foo/:bar', {}, ['/']) + # # => %r{\Afoo/(?<bar>[^/]+)\Z} + # + # Strexp.compile(':foo.example.com') + # # => %r{\A(?<foo>.+)\.example\.com\Z} + # + # Strexp.compile('foo/:bar', {:bar => /[a-z]+/}, ['/']) + # # => %r{\Afoo/(?<bar>[a-z]+)\Z} + # + # Strexp.compile('foo(.:extension)') + # # => %r{\Afoo(\.(?<extension>.+))?\Z} + # + # Strexp.compile('src/*files') + # # => %r{\Asrc/(?<files>.+)\Z} + def compile(str, requirements = {}, separators = [], anchor = true) + return Regexp.compile(str) if str.is_a?(Regexp) + + requirements = requirements ? requirements.dup : {} + normalize_requirements!(requirements, separators) + + parser = StrexpParser.new + parser.anchor = anchor + parser.requirements = requirements + + begin + re = parser.scan_str(str) + rescue Racc::ParseError => e + raise RegexpError, e.message + end + + Regexp.compile(re) + end + alias_method :new, :compile + + private + def normalize_requirements!(requirements, separators) + requirements.each do |key, value| + if value.is_a?(Regexp) + if regexp_has_modifiers?(value) + requirements[key] = value + else + requirements[key] = value.source + end + else + requirements[key] = Regexp.escape(value) + end + end + requirements.default ||= separators.any? ? + "[^#{separators.join}]+" : '.+' + requirements + end + + def regexp_has_modifiers?(regexp) + regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0 + end + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.rb new file mode 100644 index 0000000000..cfe05afc61 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.rb @@ -0,0 +1,160 @@ +# +# DO NOT MODIFY!!!! +# This file is automatically generated by Racc 1.4.6 +# from Racc grammer file "". +# + +require 'racc/parser.rb' + +require 'rack/mount/utils' +require 'rack/mount/strexp/tokenizer' + +module Rack + module Mount + class StrexpParser < Racc::Parser + + +if Regin.regexp_supports_named_captures? + REGEXP_NAMED_CAPTURE = '(?<%s>%s)'.freeze +else + REGEXP_NAMED_CAPTURE = '(?:<%s>%s)'.freeze +end + +attr_accessor :anchor, :requirements +##### State transition tables begin ### + +racc_action_table = [ + 1, 2, 3, 9, 4, 1, 2, 3, 12, 4, + 1, 2, 3, 11, 4, 1, 2, 3, nil, 4 ] + +racc_action_check = [ + 0, 0, 0, 5, 0, 3, 3, 3, 9, 3, + 8, 8, 8, 8, 8, 6, 6, 6, nil, 6 ] + +racc_action_pointer = [ + -2, nil, nil, 3, nil, 3, 13, nil, 8, 8, + nil, nil, nil ] + +racc_action_default = [ + -8, -4, -5, -8, -7, -8, -1, -3, -8, -8, + -2, -6, 13 ] + +racc_goto_table = [ + 6, 5, 10, 8, 10 ] + +racc_goto_check = [ + 2, 1, 3, 2, 3 ] + +racc_goto_pointer = [ + nil, 1, 0, -4 ] + +racc_goto_default = [ + nil, nil, nil, 7 ] + +racc_reduce_table = [ + 0, 0, :racc_error, + 1, 8, :_reduce_1, + 2, 9, :_reduce_2, + 1, 9, :_reduce_none, + 1, 10, :_reduce_4, + 1, 10, :_reduce_5, + 3, 10, :_reduce_6, + 1, 10, :_reduce_7 ] + +racc_reduce_n = 8 + +racc_shift_n = 13 + +racc_token_table = { + false => 0, + :error => 1, + :PARAM => 2, + :GLOB => 3, + :LPAREN => 4, + :RPAREN => 5, + :CHAR => 6 } + +racc_nt_base = 7 + +racc_use_result_var = true + +Racc_arg = [ + racc_action_table, + racc_action_check, + racc_action_default, + racc_action_pointer, + racc_goto_table, + racc_goto_check, + racc_goto_default, + racc_goto_pointer, + racc_nt_base, + racc_reduce_table, + racc_token_table, + racc_shift_n, + racc_reduce_n, + racc_use_result_var ] + +Racc_token_to_s_table = [ + "$end", + "error", + "PARAM", + "GLOB", + "LPAREN", + "RPAREN", + "CHAR", + "$start", + "target", + "expr", + "token" ] + +Racc_debug_parser = false + +##### State transition tables end ##### + +# reduce 0 omitted + +def _reduce_1(val, _values, result) + result = anchor ? "\\A#{val.join}\\Z" : "\\A#{val.join}" + result +end + +def _reduce_2(val, _values, result) + result = val.join + result +end + +# reduce 3 omitted + +def _reduce_4(val, _values, result) + name = val[0].to_sym + requirement = requirements[name] + result = REGEXP_NAMED_CAPTURE % [name, requirement] + + result +end + +def _reduce_5(val, _values, result) + name = val[0].to_sym + requirement = requirements[name] + result = REGEXP_NAMED_CAPTURE % [name, '.+' || requirement] + + result +end + +def _reduce_6(val, _values, result) + result = "(?:#{val[1]})?" + result +end + +def _reduce_7(val, _values, result) + result = Regexp.escape(val[0]) + result +end + +def _reduce_none(val, _values, result) + val[0] +end + + end # class StrexpParser + end # module Mount +end # module Rack diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.y b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.y new file mode 100644 index 0000000000..ffbd9fae11 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.y @@ -0,0 +1,34 @@ +class Rack::Mount::StrexpParser +rule + target: expr { result = anchor ? "\\A#{val.join}\\Z" : "\\A#{val.join}" } + + expr: expr token { result = val.join } + | token + + token: PARAM { + name = val[0].to_sym + requirement = requirements[name] + result = REGEXP_NAMED_CAPTURE % [name, requirement] + } + | GLOB { + name = val[0].to_sym + requirement = requirements[name] + result = REGEXP_NAMED_CAPTURE % [name, '.+' || requirement] + } + | LPAREN expr RPAREN { result = "(?:#{val[1]})?" } + | CHAR { result = Regexp.escape(val[0]) } +end + +---- header ---- +require 'rack/mount/utils' +require 'rack/mount/strexp/tokenizer' + +---- inner + +if Regin.regexp_supports_named_captures? + REGEXP_NAMED_CAPTURE = '(?<%s>%s)'.freeze +else + REGEXP_NAMED_CAPTURE = '(?:<%s>%s)'.freeze +end + +attr_accessor :anchor, :requirements diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rb new file mode 100644 index 0000000000..0ff7f67661 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rb @@ -0,0 +1,83 @@ +#-- +# DO NOT MODIFY!!!! +# This file is automatically generated by rex 1.0.5.beta1 +# from lexical definition file "lib/rack/mount/strexp/tokenizer.rex". +#++ + +require 'racc/parser' +class Rack::Mount::StrexpParser < Racc::Parser + require 'strscan' + + class ScanError < StandardError ; end + + attr_reader :lineno + attr_reader :filename + attr_accessor :state + + def scan_setup(str) + @ss = StringScanner.new(str) + @lineno = 1 + @state = nil + end + + def action + yield + end + + def scan_str(str) + scan_setup(str) + do_parse + end + alias :scan :scan_str + + def load_file( filename ) + @filename = filename + open(filename, "r") do |f| + scan_setup(f.read) + end + end + + def scan_file( filename ) + load_file(filename) + do_parse + end + + + def next_token + return if @ss.eos? + + text = @ss.peek(1) + @lineno += 1 if text == "\n" + token = case @state + when nil + case + when (text = @ss.scan(/\\(\(|\)|:|\*)/)) + action { [:CHAR, @ss[1]] } + + when (text = @ss.scan(/\:([a-zA-Z_]\w*)/)) + action { [:PARAM, @ss[1]] } + + when (text = @ss.scan(/\*([a-zA-Z_]\w*)/)) + action { [:GLOB, @ss[1]] } + + when (text = @ss.scan(/\(/)) + action { [:LPAREN, text] } + + when (text = @ss.scan(/\)/)) + action { [:RPAREN, text] } + + when (text = @ss.scan(/./)) + action { [:CHAR, text] } + + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '" + text + "'" + end # if + + else + raise ScanError, "undefined state: '" + state.to_s + "'" + end # case state + token + end # def next_token + +end # class diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rex b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rex new file mode 100644 index 0000000000..473bd096e1 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rex @@ -0,0 +1,12 @@ +class Rack::Mount::StrexpParser +macro + RESERVED \(|\)|:|\* + ALPHA_U [a-zA-Z_] +rule + \\({RESERVED}) { [:CHAR, @ss[1]] } + \:({ALPHA_U}\w*) { [:PARAM, @ss[1]] } + \*({ALPHA_U}\w*) { [:GLOB, @ss[1]] } + \( { [:LPAREN, text] } + \) { [:RPAREN, text] } + . { [:CHAR, text] } +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/utils.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/utils.rb new file mode 100644 index 0000000000..aa23b1162f --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/utils.rb @@ -0,0 +1,148 @@ +begin + require 'regin' +rescue LoadError + $: << File.expand_path(File.join(File.dirname(__FILE__), 'vendor/regin')) + require 'regin' +end + +require 'uri' + +module Rack::Mount + # Private utility methods used throughout Rack::Mount. + #-- + # This module is a trash can. Try to move these functions into + # more appropriate contexts. + #++ + module Utils + # Normalizes URI path. + # + # Strips off trailing slash and ensures there is a leading slash. + # + # normalize_path("/foo") # => "/foo" + # normalize_path("/foo/") # => "/foo" + # normalize_path("foo") # => "/foo" + # normalize_path("") # => "/" + def normalize_path(path) + path = "/#{path}" + path.squeeze!('/') + path.sub!(%r{/+\Z}, '') + path = '/' if path == '' + path + end + module_function :normalize_path + + # Removes trailing nils from array. + # + # pop_trailing_nils!([1, 2, 3]) # => [1, 2, 3] + # pop_trailing_nils!([1, 2, 3, nil, nil]) # => [1, 2, 3] + # pop_trailing_nils!([nil]) # => [] + def pop_trailing_nils!(ary) + while ary.length > 0 && ary.last.nil? + ary.pop + end + ary + end + module_function :pop_trailing_nils! + + RESERVED_PCHAR = ':@&=+$,;%' + SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" + if RUBY_VERSION >= '1.9' + UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze + else + UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze + end + + Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI + + def escape_uri(uri) + Parser.escape(uri.to_s, UNSAFE_PCHAR) + end + module_function :escape_uri + + if ''.respond_to?(:force_encoding) + def unescape_uri(uri) + Parser.unescape(uri).force_encoding('utf-8') + end + else + def unescape_uri(uri) + URI.unescape(uri) + end + end + module_function :unescape_uri + + # Taken from Rack 1.1.x to build nested query strings + def build_nested_query(value, prefix = nil) #:nodoc: + case value + when Array + value.map { |v| + build_nested_query(v, "#{prefix}[]") + }.join("&") + when Hash + value.map { |k, v| + build_nested_query(v, prefix ? "#{prefix}[#{Rack::Utils.escape(k)}]" : Rack::Utils.escape(k)) + }.join("&") + when String + raise ArgumentError, "value must be a Hash" if prefix.nil? + "#{prefix}=#{Rack::Utils.escape(value)}" + else + prefix + end + end + module_function :build_nested_query + + # Determines whether the regexp must match the entire string. + # + # regexp_anchored?(/^foo$/) # => true + # regexp_anchored?(/foo/) # => false + # regexp_anchored?(/^foo/) # => false + # regexp_anchored?(/foo$/) # => false + def regexp_anchored?(regexp) + regexp.source =~ /\A(\\A|\^).*(\\Z|\$)\Z/m ? true : false + end + module_function :regexp_anchored? + + def normalize_extended_expression(regexp) + return regexp unless regexp.options & Regexp::EXTENDED != 0 + source = regexp.source + source.gsub!(/#.+$/, '') + source.gsub!(/\s+/, '') + source.gsub!(/\\\//, '/') + Regexp.compile(source) + end + module_function :normalize_extended_expression + + def parse_regexp(regexp) + cache = @@_parse_regexp_cache ||= {} + + if expression = cache[regexp] + return expression + end + + unless regexp.is_a?(RegexpWithNamedGroups) + regexp = RegexpWithNamedGroups.new(regexp) + end + + expression = Regin.parse(regexp) + + unless Regin.regexp_supports_named_captures? + tag_captures = Proc.new do |group| + case group + when Regin::Group + # TODO: dup instead of mutating + group.instance_variable_set('@name', regexp.names[group.index]) if group.index + tag_captures.call(group.expression) + when Regin::Expression + group.each { |child| tag_captures.call(child) } + end + end + tag_captures.call(expression) + end + + cache[regexp] = expression.freeze + expression + rescue Racc::ParseError, Regin::Parser::ScanError + [] + end + module_function :parse_regexp + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multimap.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multimap.rb new file mode 100644 index 0000000000..0b49b49280 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multimap.rb @@ -0,0 +1,569 @@ +require 'forwardable' +require 'multiset' + +# Multimap is a generalization of a map or associative array +# abstract data type in which more than one value may be associated +# with and returned for a given key. +# +# == Example +# +# require 'multimap' +# map = Multimap.new +# map["a"] = 100 +# map["b"] = 200 +# map["a"] = 300 +# map["a"] # -> [100, 300] +# map["b"] # -> [200] +# map.keys # -> #<Multiset: {a, a, b}> +class Multimap + extend Forwardable + + include Enumerable + + # call-seq: + # Multimap[ [key =>|, value]* ] => multimap + # + # Creates a new multimap populated with the given objects. + # + # Multimap["a", 100, "b", 200] #=> {"a"=>[100], "b"=>[200]} + # Multimap["a" => 100, "b" => 200] #=> {"a"=>[100], "b"=>[200]} + def self.[](*args) + default = [] + + if args.size == 2 && args.last.is_a?(Hash) + default = args.shift + elsif !args.first.is_a?(Hash) && args.size % 2 == 1 + default = args.shift + end + + if args.size == 1 && args.first.is_a?(Hash) + args[0] = args.first.inject({}) { |hash, (key, value)| + unless value.is_a?(default.class) + value = (default.dup << value) + end + hash[key] = value + hash + } + else + index = 0 + args.map! { |value| + unless index % 2 == 0 || value.is_a?(default.class) + value = (default.dup << value) + end + index += 1 + value + } + end + + map = new + map.instance_variable_set(:@hash, Hash[*args]) + map.default = default + map + end + + # call-seq: + # Multimap.new => multimap + # Multimap.new(default) => multimap + # + # Returns a new, empty multimap. + # + # map = Multimap.new(Set.new) + # h["a"] = 100 + # h["b"] = 200 + # h["a"] #=> [100].to_set + # h["c"] #=> [].to_set + def initialize(default = []) + @hash = Hash.new(default) + end + + def initialize_copy(original) #:nodoc: + @hash = Hash.new(original.default.dup) + original._internal_hash.each_pair do |key, container| + @hash[key] = container.dup + end + end + + def_delegators :@hash, :clear, :default, :default=, :empty?, + :fetch, :has_key?, :key? + + # Retrieves the <i>value</i> object corresponding to the + # <i>*keys</i> object. + def [](key) + @hash[key] + end + + # call-seq: + # map[key] = value => value + # map.store(key, value) => value + # + # Associates the value given by <i>value</i> with the key + # given by <i>key</i>. Unlike a regular hash, multiple can be + # assoicated with the same value. + # + # map = Multimap["a" => 100, "b" => 200] + # map["a"] = 9 + # map["c"] = 4 + # map #=> {"a" => [100, 9], "b" => [200], "c" => [4]} + def store(key, value) + update_container(key) do |container| + container << value + container + end + end + alias_method :[]=, :store + + # call-seq: + # map.delete(key, value) => value + # map.delete(key) => value + # + # Deletes and returns a key-value pair from <i>map</i>. If only + # <i>key</i> is given, all the values matching that key will be + # deleted. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.delete("b", 300) #=> 300 + # map.delete("a") #=> [100] + def delete(key, value = nil) + if value + @hash[key].delete(value) + else + @hash.delete(key) + end + end + + # call-seq: + # map.each { |key, value| block } => map + # + # Calls <i>block</i> for each key/value pair in <i>map</i>, passing + # the key and value to the block as a two-element array. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.each { |key, value| puts "#{key} is #{value}" } + # + # <em>produces:</em> + # + # a is 100 + # b is 200 + # b is 300 + def each + each_pair do |key, value| + yield [key, value] + end + end + + # call-seq: + # map.each_association { |key, container| block } => map + # + # Calls <i>block</i> once for each key/container in <i>map</i>, passing + # the key and container to the block as parameters. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.each_association { |key, container| puts "#{key} is #{container}" } + # + # <em>produces:</em> + # + # a is [100] + # b is [200, 300] + def each_association(&block) + @hash.each_pair(&block) + end + + # call-seq: + # map.each_container { |container| block } => map + # + # Calls <i>block</i> for each container in <i>map</i>, passing the + # container as a parameter. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.each_container { |container| puts container } + # + # <em>produces:</em> + # + # [100] + # [200, 300] + def each_container + each_association do |_, container| + yield container + end + end + + # call-seq: + # map.each_key { |key| block } => map + # + # Calls <i>block</i> for each key in <i>hsh</i>, passing the key + # as a parameter. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.each_key { |key| puts key } + # + # <em>produces:</em> + # + # a + # b + # b + def each_key + each_pair do |key, _| + yield key + end + end + + # call-seq: + # map.each_pair { |key_value_array| block } => map + # + # Calls <i>block</i> for each key/value pair in <i>map</i>, + # passing the key and value as parameters. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.each_pair { |key, value| puts "#{key} is #{value}" } + # + # <em>produces:</em> + # + # a is 100 + # b is 200 + # b is 300 + def each_pair + each_association do |key, values| + values.each do |value| + yield key, value + end + end + end + + # call-seq: + # map.each_value { |value| block } => map + # + # Calls <i>block</i> for each key in <i>map</i>, passing the + # value as a parameter. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.each_value { |value| puts value } + # + # <em>produces:</em> + # + # 100 + # 200 + # 300 + def each_value + each_pair do |_, value| + yield value + end + end + + def ==(other) #:nodoc: + case other + when Multimap + @hash == other._internal_hash + else + @hash == other + end + end + + def eql?(other) #:nodoc: + case other + when Multimap + @hash.eql?(other._internal_hash) + else + @hash.eql?(other) + end + end + + def freeze #:nodoc: + each_container { |container| container.freeze } + default.freeze + super + end + + # call-seq: + # map.has_value?(value) => true or false + # map.value?(value) => true or false + # + # Returns <tt>true</tt> if the given value is present for any key + # in <i>map</i>. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.has_value?(300) #=> true + # map.has_value?(999) #=> false + def has_value?(value) + values.include?(value) + end + alias_method :value?, :has_value? + + # call-seq: + # map.index(value) => key + # + # Returns the key for a given value. If not found, returns + # <tt>nil</tt>. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.index(100) #=> "a" + # map.index(200) #=> "b" + # map.index(999) #=> nil + def index(value) + invert[value] + end + + # call-seq: + # map.delete_if {| key, value | block } -> map + # + # Deletes every key-value pair from <i>map</i> for which <i>block</i> + # evaluates to <code>true</code>. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.delete_if {|key, value| value >= 300 } + # #=> Multimap["a" => 100, "b" => 200] + # + def delete_if + each_association do |key, container| + container.delete_if do |value| + yield [key, value] + end + end + self + end + + # call-seq: + # map.reject {| key, value | block } -> map + # + # Same as <code>Multimap#delete_if</code>, but works on (and returns) a + # copy of the <i>map</i>. Equivalent to + # <code><i>map</i>.dup.delete_if</code>. + # + def reject(&block) + dup.delete_if(&block) + end + + # call-seq: + # map.reject! {| key, value | block } -> map or nil + # + # Equivalent to <code>Multimap#delete_if</code>, but returns + # <code>nil</code> if no changes were made. + # + def reject!(&block) + old_size = size + delete_if(&block) + old_size == size ? nil : self + end + + # call-seq: + # map.replace(other_map) => map + # + # Replaces the contents of <i>map</i> with the contents of + # <i>other_map</i>. + # + # map = Multimap["a" => 100, "b" => 200] + # map.replace({ "c" => 300, "d" => 400 }) + # #=> Multimap["c" => 300, "d" => 400] + def replace(other) + case other + when Array + @hash.replace(self.class[self.default, *other]) + when Hash + @hash.replace(self.class[self.default, other]) + when self.class + @hash.replace(other) + else + raise ArgumentError + end + end + + # call-seq: + # map.invert => multimap + # + # Returns a new multimap created by using <i>map</i>'s values as keys, + # and the keys as values. + # + # map = Multimap["n" => 100, "m" => 100, "d" => [200, 300]] + # map.invert #=> Multimap[100 => ["n", "m"], 200 => "d", 300 => "d"] + def invert + h = self.class.new(default.dup) + each_pair { |key, value| h[value] = key } + h + end + + # call-seq: + # map.keys => multiset + # + # Returns a new +Multiset+ populated with the keys from this hash. See also + # <tt>Multimap#values</tt> and <tt>Multimap#containers</tt>. + # + # map = Multimap["a" => 100, "b" => [200, 300], "c" => 400] + # map.keys #=> Multiset.new(["a", "b", "b", "c"]) + def keys + keys = Multiset.new + each_key { |key| keys << key } + keys + end + + # Returns true if the given key is present in Multimap. + def include?(key) + keys.include?(key) + end + alias_method :member?, :include? + + # call-seq: + # map.length => fixnum + # map.size => fixnum + # + # Returns the number of key-value pairs in the map. + # + # map = Multimap["a" => 100, "b" => [200, 300], "c" => 400] + # map.length #=> 4 + # map.delete("a") #=> 100 + # map.length #=> 3 + def size + values.size + end + alias_method :length, :size + + # call-seq: + # map.merge(other_map) => multimap + # + # Returns a new multimap containing the contents of <i>other_map</i> and + # the contents of <i>map</i>. + # + # map1 = Multimap["a" => 100, "b" => 200] + # map2 = Multimap["a" => 254, "c" => 300] + # map2.merge(map2) #=> Multimap["a" => 100, "b" => [200, 254], "c" => 300] + # map1 #=> Multimap["a" => 100, "b" => 200] + def merge(other) + dup.update(other) + end + + # call-seq: + # map.merge!(other_map) => multimap + # map.update(other_map) => multimap + # + # Adds each pair from <i>other_map</i> to <i>map</i>. + # + # map1 = Multimap["a" => 100, "b" => 200] + # map2 = Multimap["b" => 254, "c" => 300] + # + # map1.merge!(map2) + # #=> Multimap["a" => 100, "b" => [200, 254], "c" => 300] + def update(other) + case other + when self.class + other.each_pair { |key, value| store(key, value) } + when Hash + update(self.class[self.default, other]) + else + raise ArgumentError + end + self + end + alias_method :merge!, :update + + # call-seq: + # map.select { |key, value| block } => multimap + # + # Returns a new Multimap consisting of the pairs for which the + # block returns true. + # + # map = Multimap["a" => 100, "b" => 200, "c" => 300] + # map.select { |k,v| k > "a" } #=> Multimap["b" => 200, "c" => 300] + # map.select { |k,v| v < 200 } #=> Multimap["a" => 100] + def select + inject(self.class.new) { |map, (key, value)| + map[key] = value if yield([key, value]) + map + } + end + + # call-seq: + # map.to_a => array + # + # Converts <i>map</i> to a nested array of [<i>key, + # value</i>] arrays. + # + # map = Multimap["a" => 100, "b" => [200, 300], "c" => 400] + # map.to_a #=> [["a", 100], ["b", 200], ["b", 300], ["c", 400]] + def to_a + ary = [] + each_pair do |key, value| + ary << [key, value] + end + ary + end + + # call-seq: + # map.to_hash => hash + # + # Converts <i>map</i> to a basic hash. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.to_hash #=> { "a" => [100], "b" => [200, 300] } + def to_hash + @hash.dup + end + + # call-seq: + # map.containers => array + # + # Returns a new array populated with the containers from <i>map</i>. See + # also <tt>Multimap#keys</tt> and <tt>Multimap#values</tt>. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.containers #=> [[100], [200, 300]] + def containers + containers = [] + each_container { |container| containers << container } + containers + end + + # call-seq: + # map.values => array + # + # Returns a new array populated with the values from <i>map</i>. See + # also <tt>Multimap#keys</tt> and <tt>Multimap#containers</tt>. + # + # map = Multimap["a" => 100, "b" => [200, 300]] + # map.values #=> [100, 200, 300] + def values + values = [] + each_value { |value| values << value } + values + end + + # Return an array containing the values associated with the given keys. + def values_at(*keys) + @hash.values_at(*keys) + end + + def marshal_dump #:nodoc: + @hash + end + + def marshal_load(hash) #:nodoc: + @hash = hash + end + + def to_yaml(opts = {}) #:nodoc: + YAML::quick_emit(self, opts) do |out| + out.map(taguri, to_yaml_style) do |map| + @hash.each do |k, v| + map.add(k, v) + end + map.add('__default__', @hash.default) + end + end + end + + def yaml_initialize(tag, val) #:nodoc: + default = val.delete('__default__') + @hash = val + @hash.default = default + self + end + + protected + def _internal_hash #:nodoc: + @hash + end + + def update_container(key) #:nodoc: + container = @hash[key] + container = container.dup if container.equal?(default) + container = yield(container) + @hash[key] = container + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multiset.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multiset.rb new file mode 100644 index 0000000000..119bf12646 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multiset.rb @@ -0,0 +1,185 @@ +require 'set' + +# Multiset implements a collection of unordered values and +# allows duplicates. +# +# == Example +# +# require 'multiset' +# s1 = Multiset.new [1, 2] # -> #<Multiset: {1, 2}> +# s1.add(2) # -> #<Multiset: {1, 2, 2}> +# s1.merge([2, 6]) # -> #<Multiset: {1, 2, 2, 2, 3}> +# s1.multiplicity(2) # -> 3 +# s1.multiplicity(3) # -> 1 +class Multiset < Set + def initialize(*args, &block) #:nodoc: + @hash = Hash.new(0) + super + end + + # Returns the number of times an element belongs to the multiset. + def multiplicity(e) + @hash[e] + end + + # Returns the total number of elements in a multiset, including + # repeated memberships + def cardinality + @hash.inject(0) { |s, (e, m)| s += m } + end + alias_method :size, :cardinality + alias_method :length, :cardinality + + # Converts the set to an array. The order of elements is uncertain. + def to_a + inject([]) { |ary, (key, _)| ary << key } + end + + # Returns true if the set is a superset of the given set. + def superset?(set) + set.is_a?(self.class) or raise ArgumentError, "value must be a set" + return false if cardinality < set.cardinality + set.all? { |o| set.multiplicity(o) <= multiplicity(o) } + end + + # Returns true if the set is a proper superset of the given set. + def proper_superset?(set) + set.is_a?(self.class) or raise ArgumentError, "value must be a set" + return false if cardinality <= set.cardinality + set.all? { |o| set.multiplicity(o) <= multiplicity(o) } + end + + # Returns true if the set is a subset of the given set. + def subset?(set) + set.is_a?(self.class) or raise ArgumentError, "value must be a set" + return false if set.cardinality < cardinality + all? { |o| multiplicity(o) <= set.multiplicity(o) } + end + + # Returns true if the set is a proper subset of the given set. + def proper_subset?(set) + set.is_a?(self.class) or raise ArgumentError, "value must be a set" + return false if set.cardinality <= cardinality + all? { |o| multiplicity(o) <= set.multiplicity(o) } + end + + # Calls the given block once for each element in the set, passing + # the element as parameter. Returns an enumerator if no block is + # given. + def each + @hash.each_pair do |key, multiplicity| + multiplicity.times do + yield(key) + end + end + self + end + + # Adds the given object to the set and returns self. Use +merge+ to + # add many elements at once. + def add(o) + @hash[o] ||= 0 + @hash[o] += 1 + self + end + alias << add + + undef :add? + + # Deletes all the identical object from the set and returns self. + # If +n+ is given, it will remove that amount of identical objects + # from the set. Use +subtract+ to delete many different items at + # once. + def delete(o, n = nil) + if n + @hash[o] ||= 0 + @hash[o] -= n if @hash[o] > 0 + @hash.delete(o) if @hash[o] == 0 + else + @hash.delete(o) + end + self + end + + undef :delete? + + # Deletes every element of the set for which block evaluates to + # true, and returns self. + def delete_if + each { |o| delete(o) if yield(o) } + self + end + + # Merges the elements of the given enumerable object to the set and + # returns self. + def merge(enum) + enum.each { |o| add(o) } + self + end + + # Deletes every element that appears in the given enumerable object + # and returns self. + def subtract(enum) + enum.each { |o| delete(o, 1) } + self + end + + # Returns a new set containing elements common to the set and the + # given enumerable object. + def &(enum) + s = dup + n = self.class.new + enum.each { |o| + if s.include?(o) + s.delete(o, 1) + n.add(o) + end + } + n + end + alias intersection & + + # Returns a new set containing elements exclusive between the set + # and the given enumerable object. (set ^ enum) is equivalent to + # ((set | enum) - (set & enum)). + def ^(enum) + n = self.class.new(enum) + each { |o| n.include?(o) ? n.delete(o, 1) : n.add(o) } + n + end + + # Returns true if two sets are equal. Two multisets are equal if + # they have the same cardinalities and each element has the same + # multiplicity in both sets. The equality of each element inside + # the multiset is defined according to Object#eql?. + def eql?(set) + return true if equal?(set) + set = self.class.new(set) unless set.is_a?(self.class) + return false unless cardinality == set.cardinality + superset?(set) && subset?(set) + end + alias_method :==, :eql? + + def marshal_dump #:nodoc: + @hash + end + + def marshal_load(hash) #:nodoc: + @hash = hash + end + + def to_yaml(opts = {}) #:nodoc: + YAML::quick_emit(self, opts) do |out| + out.map(taguri, to_yaml_style) do |map| + @hash.each do |k, v| + map.add(k, v) + end + end + end + end + + def yaml_initialize(tag, val) #:nodoc: + @hash = val + self + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/nested_multimap.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/nested_multimap.rb new file mode 100644 index 0000000000..4eb088b91a --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/nested_multimap.rb @@ -0,0 +1,158 @@ +require 'multimap' + +# NestedMultimap allows values to be assoicated with a nested +# set of keys. +class NestedMultimap < Multimap + # call-seq: + # multimap[*keys] = value => value + # multimap.store(*keys, value) => value + # + # Associates the value given by <i>value</i> with multiple key + # given by <i>keys</i>. + # + # map = NestedMultimap.new + # map["a"] = 100 + # map["a", "b"] = 101 + # map["a"] = 102 + # map #=> {"a"=>{"b"=>[100, 101, 102], default => [100, 102]}} + def store(*args) + keys = args + value = args.pop + + raise ArgumentError, 'wrong number of arguments (1 for 2)' unless value + + if keys.length > 1 + update_container(keys.shift) do |container| + container = self.class.new(container) unless container.is_a?(self.class) + container[*keys] = value + container + end + elsif keys.length == 1 + super(keys.first, value) + else + self << value + end + end + alias_method :[]=, :store + + # call-seq: + # multimap << obj => multimap + # + # Pushes the given object on to the end of all the containers. + # + # map = NestedMultimap["a" => [100], "b" => [200, 300]] + # map << 300 + # map["a"] #=> [100, 300] + # map["c"] #=> [300] + def <<(value) + @hash.each_value { |container| container << value } + self.default << value + self + end + + # call-seq: + # multimap[*keys] => value + # multimap[key1, key2, key3] => value + # + # Retrieves the <i>value</i> object corresponding to the + # <i>*keys</i> object. + def [](*keys) + i, l, r, k = 0, keys.length, self, self.class + while r.is_a?(k) + r = i < l ? r._internal_hash[keys[i]] : r.default + i += 1 + end + r + end + + # call-seq: + # multimap.each_association { |key, container| block } => multimap + # + # Calls <i>block</i> once for each key/container in <i>map</i>, passing + # the key and container to the block as parameters. + # + # map = NestedMultimap.new + # map["a"] = 100 + # map["a", "b"] = 101 + # map["a"] = 102 + # map["c"] = 200 + # map.each_association { |key, container| puts "#{key} is #{container}" } + # + # <em>produces:</em> + # + # ["a", "b"] is [100, 101, 102] + # "c" is [200] + def each_association + super() do |key, container| + if container.respond_to?(:each_association) + container.each_association do |nested_key, value| + yield [key, nested_key].flatten, value + end + else + yield key, container + end + end + end + + # call-seq: + # multimap.each_container_with_default { |container| block } => map + # + # Calls <i>block</i> for every container in <i>map</i> including + # the default, passing the container as a parameter. + # + # map = NestedMultimap.new + # map["a"] = 100 + # map["a", "b"] = 101 + # map["a"] = 102 + # map.each_container_with_default { |container| puts container } + # + # <em>produces:</em> + # + # [100, 101, 102] + # [100, 102] + # [] + def each_container_with_default(&block) + @hash.each_value do |container| + iterate_over_container(container, &block) + end + iterate_over_container(default, &block) + self + end + + # call-seq: + # multimap.containers_with_default => array + # + # Returns a new array populated with all the containers from + # <i>map</i> including the default. + # + # map = NestedMultimap.new + # map["a"] = 100 + # map["a", "b"] = 101 + # map["a"] = 102 + # map.containers_with_default #=> [[100, 101, 102], [100, 102], []] + def containers_with_default + containers = [] + each_container_with_default { |container| containers << container } + containers + end + + def inspect #:nodoc: + super.gsub(/\}$/, ", default => #{default.inspect}}") + end + + private + def iterate_over_container(container) + if container.respond_to?(:each_container_with_default) + container.each_container_with_default do |value| + yield value + end + else + yield container + end + end +end + +begin + require 'nested_multimap_ext' +rescue LoadError +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin.rb new file mode 100644 index 0000000000..d38922bcc6 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin.rb @@ -0,0 +1,45 @@ +module Regin + autoload :Alternation, 'regin/alternation' + autoload :Anchor, 'regin/anchor' + autoload :Atom, 'regin/atom' + autoload :Character, 'regin/character' + autoload :CharacterClass, 'regin/character_class' + autoload :Collection, 'regin/collection' + autoload :Expression, 'regin/expression' + autoload :Group, 'regin/group' + autoload :Options, 'regin/options' + autoload :Parser, 'regin/parser' + + class << self + begin + eval('foo = /(?<foo>.*)/').named_captures + + # Returns true if the interpreter is using the Oniguruma Regexp lib + # and supports named captures. + # + # /(?<foo>bar)/ + def regexp_supports_named_captures? + true + end + rescue SyntaxError, NoMethodError + def regexp_supports_named_captures? #:nodoc: + false + end + end + + # Parses Regexp and returns a Expression data structure. + def parse(regexp) + Parser.parse_regexp(regexp) + end + + # Recompiles Regexp by parsing it and turning it back into a Regexp. + # + # (In the future Regin will perform some Regexp optimizations + # such as removing unnecessary captures and options) + def compile(source) + regexp = Regexp.compile(source) + expression = parse(regexp) + Regexp.compile(expression.to_s(true), expression.flags) + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/alternation.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/alternation.rb new file mode 100644 index 0000000000..ce4f52bfdb --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/alternation.rb @@ -0,0 +1,40 @@ +module Regin + class Alternation < Collection + def initialize(*args) + args, options = extract_options(args) + + if args.length == 1 && args.first.instance_of?(Array) + super(args.first) + else + super(args) + end + + if options.key?(:ignorecase) + @array.map! { |e| e.dup(:ignorecase => options[:ignorecase]) } + end + end + + # Returns true if expression could be treated as a literal string. + # + # Alternation groups are never literal. + def literal? + false + end + + def flags + 0 + end + + def dup(options = {}) + self.class.new(to_a, options) + end + + def to_s(parent = false) + map { |e| e.to_s(parent) }.join('|') + end + + def inspect #:nodoc: + to_s.inspect + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/anchor.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/anchor.rb new file mode 100644 index 0000000000..05520dd5e0 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/anchor.rb @@ -0,0 +1,4 @@ +module Regin + class Anchor < Atom + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/atom.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/atom.rb new file mode 100644 index 0000000000..eb1923a5a1 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/atom.rb @@ -0,0 +1,59 @@ +module Regin + class Atom + attr_reader :value, :ignorecase + + def initialize(value, options = {}) + @value = value + @ignorecase = options[:ignorecase] + end + + def option_names + %w( ignorecase ) + end + + # Returns true if expression could be treated as a literal string. + def literal? + false + end + + def casefold? + ignorecase ? true : false + end + + def dup(options = {}) + original_options = option_names.inject({}) do |h, m| + h[m.to_sym] = send(m) + h + end + self.class.new(value, original_options.merge(options)) + end + + def to_s(parent = false) + "#{value}" + end + + def inspect #:nodoc: + "#<#{self.class.to_s.sub('Regin::', '')} #{to_s.inspect}>" + end + + def ==(other) #:nodoc: + case other + when String + other == to_s + else + eql?(other) + end + end + + def eql?(other) #:nodoc: + other.instance_of?(self.class) && + self.value.eql?(other.value) && + (!!self.ignorecase).eql?(!!other.ignorecase) + end + + def freeze #:nodoc: + value.freeze + super + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character.rb new file mode 100644 index 0000000000..12a9199d2a --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character.rb @@ -0,0 +1,56 @@ +module Regin + class Character < Atom + attr_reader :quantifier + + def initialize(value, options = {}) + @quantifier = options[:quantifier] + super + end + + def option_names + %w( quantifier ) + super + end + + # Returns true if expression could be treated as a literal string. + # + # A Character is literal is there is no quantifier attached to it. + def literal? + quantifier.nil? && !ignorecase + end + + def to_s(parent = false) + if !parent && ignorecase + "(?i-mx:#{value})#{quantifier}" + else + "#{value}#{quantifier}" + end + end + + def to_regexp(anchored = false) + re = to_s(true) + re = "\\A#{re}\\Z" if anchored + Regexp.compile(re, ignorecase) + end + + def match(char) + to_regexp(true).match(char) + end + + def include?(char) + if ignorecase + value.downcase == char.downcase + else + value == char + end + end + + def eql?(other) #:nodoc: + super && quantifier.eql?(other.quantifier) + end + + def freeze #:nodoc: + quantifier.freeze if quantifier + super + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character_class.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character_class.rb new file mode 100644 index 0000000000..caed5ef9d0 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character_class.rb @@ -0,0 +1,55 @@ +module Regin + class CharacterClass < Character + def initialize(value, options = {}) + @negate = options[:negate] + super + end + + def option_names + %w( negate ) + super + end + + attr_reader :negate + + def negated? + negate ? true : false + end + + # Returns true if expression could be treated as a literal string. + # + # A CharacterClass is never literal. + def literal? + false + end + + def bracketed? + value != '.' && value !~ /^\\[dDsSwW]$/ + end + + def to_s(parent = false) + if bracketed? + if !parent && ignorecase + "(?i-mx:[#{negate && '^'}#{value}])#{quantifier}" + else + "[#{negate && '^'}#{value}]#{quantifier}" + end + else + super + end + end + + def include?(char) + re = quantifier ? to_s.sub(/#{Regexp.escape(quantifier)}$/, '') : to_s + Regexp.compile("\\A#{re}\\Z").match(char) + end + + def eql?(other) #:nodoc: + super && negate == other.negate + end + + def freeze #:nodoc: + negate.freeze if negate + super + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/collection.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/collection.rb new file mode 100644 index 0000000000..b60353268a --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/collection.rb @@ -0,0 +1,83 @@ +module Regin + class Collection + include Enumerable + + def initialize(*args) + @array = Array.new(*args) + end + + def each + @array.each{ |item| yield item } + end + + def [](i) + @array[i] + end + + def length + @array.length + end + alias_method :size, :length + + def first + @array.first + end + + def last + @array.last + end + + def +(other) + ary = other.is_a?(self.class) ? other.internal_array : other + self.class.new(@array + ary) + end + + def to_regexp(anchored = false) + re = to_s(true) + re = "\\A#{re}\\Z" if anchored + Regexp.compile(re, flags) + end + + def match(char) + to_regexp.match(char) + end + + def include?(char) + any? { |e| e.include?(char) } + end + + def ==(other) #:nodoc: + case other + when String + other == to_s + when Array + other == @array + else + eql?(other) + end + end + + def eql?(other) #:nodoc: + other.instance_of?(self.class) && @array.eql?(other.internal_array) + end + + def freeze #:nodoc: + each { |e| e.freeze } + @array.freeze + super + end + + protected + def internal_array #:nodoc: + @array + end + + def extract_options(args) + if args.last.is_a?(Hash) + return args[0..-2], args.last + else + return args, {} + end + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/expression.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/expression.rb new file mode 100644 index 0000000000..18e4965097 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/expression.rb @@ -0,0 +1,126 @@ +module Regin + class Expression < Collection + attr_reader :ignorecase, :multiline, :extended + + def initialize(*args) + args, options = extract_options(args) + + @multiline = @ignorecase = @extended = nil + + if args.length == 1 && args.first.instance_of?(Array) + super(args.first) + else + args = args.map { |e| e.instance_of?(String) ? Character.new(e) : e } + super(args) + end + + self.multiline = options[:multiline] if options.key?(:multiline) + self.ignorecase = options[:ignorecase] if options.key?(:ignorecase) + self.extended = options[:extended] if options.key?(:extended) + end + + # Returns true if expression could be treated as a literal string. + # + # A Expression is literal if all its elements are literal. + def literal? + !ignorecase && all? { |e| e.literal? } + end + + def anchored? + anchored_to_start? && anchored_to_end? + end + + def anchored_to_start? + first.is_a?(Anchor) && first == '\A' + end + + def anchored_to_end? + last.is_a?(Anchor) && last == '\Z' + end + + def anchored_to_line? + anchored_to_start_of_line? && anchored_to_end_of_line? + end + + def anchored_to_start_of_line? + anchored_to_start? || (first.is_a?(Anchor) && first == '^') + end + + def anchored_to_end_of_line? + anchored_to_end? || (last.is_a?(Anchor) && last == '$') + end + + def options? + options.any?(true) + end + + def flags + options.to_i + end + + def +(other) + ary = other.is_a?(self.class) ? other.internal_array : other + ary = @array + ary + [options.to_h(true)] + self.class.new(*ary) + end + + def dup(options = {}) + expression = super() + expression.multiline = options[:multiline] if options.key?(:multiline) + expression.ignorecase = options[:ignorecase] if options.key?(:ignorecase) + expression.extended = options[:extended] if options.key?(:extended) + expression + end + + def to_s(parent = false) + if parent || !options? + map { |e| e.to_s(parent) }.join + else + with, without = [], [] + multiline ? (with << 'm') : (without << 'm') + ignorecase ? (with << 'i') : (without << 'i') + extended ? (with << 'x') : (without << 'x') + + with = with.join + without = without.any? ? "-#{without.join}" : '' + + "(?#{with}#{without}:#{map { |e| e.to_s(true) }.join})" + end + end + + def inspect #:nodoc: + "#<Expression #{to_s.inspect}>" + end + + def casefold? + ignorecase + end + + def eql?(other) #:nodoc: + super && + !!self.multiline == !!other.multiline && + !!self.ignorecase == !!other.ignorecase && + !!self.extended == !!other.extended + end + + protected + def options + Options.new(multiline, ignorecase, extended) + end + + def multiline=(multiline) + @multiline = multiline + end + + def ignorecase=(ignorecase) + if @ignorecase.nil? + @array.map! { |e| e.dup(:ignorecase => ignorecase) } + @ignorecase = ignorecase + end + end + + def extended=(extended) + @extended = extended + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/group.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/group.rb new file mode 100644 index 0000000000..d682148bd9 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/group.rb @@ -0,0 +1,90 @@ +module Regin + class Group + attr_reader :expression, :quantifier, :capture, :index, :name + + def initialize(expression, options = {}) + @quantifier = @index = @name = nil + @capture = true + @expression = expression.dup(options) + + @quantifier = options[:quantifier] if options.key?(:quantifier) + @capture = options[:capture] if options.key?(:capture) + @index = options[:index] if options.key?(:index) + @name = options[:name] if options.key?(:name) + end + + def option_names + %w( quantifier capture index name ) + end + + # Returns true if expression could be treated as a literal string. + # + # A Group is literal if its expression is literal and it has no quantifier. + def literal? + quantifier.nil? && expression.literal? + end + + def to_s(parent = false) + if !expression.options? + "(#{capture ? '' : '?:'}#{expression.to_s(parent)})#{quantifier}" + elsif capture == false + "#{expression.to_s}#{quantifier}" + else + "(#{expression.to_s})#{quantifier}" + end + end + + def to_regexp(anchored = false) + re = to_s + re = "\\A#{re}\\Z" if anchored + Regexp.compile(re) + end + + def dup(options = {}) + original_options = option_names.inject({}) do |h, m| + h[m.to_sym] = send(m) + h + end + self.class.new(expression, original_options.merge(options)) + end + + def inspect #:nodoc: + to_s.inspect + end + + def match(char) + to_regexp.match(char) + end + + def include?(char) + expression.include?(char) + end + + def capture? + capture + end + + def ==(other) #:nodoc: + case other + when String + other == to_s + else + eql?(other) + end + end + + def eql?(other) #:nodoc: + other.is_a?(self.class) && + self.expression == other.expression && + self.quantifier == other.quantifier && + self.capture == other.capture && + self.index == other.index && + self.name == other.name + end + + def freeze #:nodoc: + expression.freeze if expression + super + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/options.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/options.rb new file mode 100644 index 0000000000..03ba29d9a5 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/options.rb @@ -0,0 +1,55 @@ +module Regin + class Options + def self.from_int(flags) + multiline = flags & Regexp::MULTILINE != 0 + ignorecase = flags & Regexp::IGNORECASE != 0 + extended = flags & Regexp::EXTENDED != 0 + + new(multiline, ignorecase, extended) + end + + attr_reader :multiline, :ignorecase, :extended + + def initialize(*args) + if args.first.is_a?(Hash) + @multiline = args[0][:multiline] + @ignorecase = args[0][:ignorecase] + @extended = args[0][:extended] + else + @multiline = args[0] + @ignorecase = args[1] + @extended = args[2] + end + end + + def any?(explicit = false) + if explicit + !multiline.nil? || !ignorecase.nil? || !extended.nil? + else + multiline || ignorecase || extended + end + end + + def to_h(explicit = false) + if explicit + options = {} + options[:multiline] = multiline unless multiline.nil? + options[:ignorecase] = ignorecase unless ignorecase.nil? + options[:extended] = extended unless extended.nil? + options + else + { :multiline => multiline, + :ignorecase => ignorecase, + :extended => extended } + end + end + + def to_i + flag = 0 + flag |= Regexp::MULTILINE if multiline + flag |= Regexp::IGNORECASE if ignorecase + flag |= Regexp::EXTENDED if extended + flag + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/parser.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/parser.rb new file mode 100644 index 0000000000..0bb9b87e9c --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/parser.rb @@ -0,0 +1,415 @@ +# +# DO NOT MODIFY!!!! +# This file is automatically generated by Racc 1.4.6 +# from Racc grammer file "". +# + +require 'racc/parser.rb' +module Regin + class Parser < Racc::Parser #:nodoc: all + +def self.parse_regexp(regexp) + options = Options.from_int(regexp.options) + + parser = new + parser.options_stack << options.to_h + + expression = parser.scan_str(regexp.source) + expression = expression.dup(options.to_h) if options.any? + expression +end + +attr_accessor :options_stack + +def initialize + @capture_index = 0 + @capture_index_stack = [] + @options_stack = [] +end + +##### State transition tables begin ### + +racc_action_table = [ + 2, 18, 19, 19, 8, 10, 11, 13, 48, 19, + 2, 45, 3, 5, 8, 10, 11, 13, 64, 47, + 2, 55, 3, 5, 8, 10, 11, 13, 29, 19, + 2, 16, 3, 5, 8, 10, 11, 13, 61, 19, + 2, 63, 3, 5, 8, 10, 11, 13, 60, 36, + 2, 34, 3, 5, 8, 10, 11, 13, 28, 49, + 2, nil, 3, 5, 8, 10, 11, 13, nil, nil, + 2, nil, 3, 5, 8, 10, 11, 13, nil, 26, + 42, 43, 3, 5, 37, 38, 40, 21, 44, 37, + 38, 40, 22, 23, 24, 14, nil, 15, 31, 32, + 16, nil, 33, 46, 32, nil, nil, 33, 51, 37, + 38, 40, 58, 37, 38, 40, 37, 38, 40, 37, + 38, 40, 37, 38, 40, 37, 38, 40, 37, 38, + 40 ] + +racc_action_check = [ + 0, 4, 27, 4, 0, 0, 0, 0, 36, 56, + 49, 27, 0, 0, 49, 49, 49, 49, 56, 36, + 43, 48, 49, 49, 43, 43, 43, 43, 15, 53, + 6, 15, 43, 43, 6, 6, 6, 6, 53, 52, + 42, 55, 6, 6, 42, 42, 42, 42, 52, 24, + 35, 18, 42, 42, 35, 35, 35, 35, 14, 39, + 19, nil, 35, 35, 19, 19, 19, 19, nil, nil, + 13, nil, 19, 19, 13, 13, 13, 13, nil, 13, + 26, 26, 13, 13, 54, 54, 54, 9, 26, 26, + 26, 26, 9, 9, 9, 2, nil, 2, 17, 17, + 2, nil, 17, 30, 30, nil, nil, 30, 41, 41, + 41, 41, 50, 50, 50, 50, 44, 44, 44, 59, + 59, 59, 58, 58, 58, 51, 51, 51, 62, 62, + 62 ] + +racc_action_pointer = [ + -3, nil, 91, nil, 1, nil, 27, nil, nil, 75, + nil, nil, nil, 67, 53, 22, nil, 93, 51, 57, + nil, nil, nil, nil, 40, nil, 67, 0, nil, nil, + 98, nil, nil, nil, nil, 47, -1, nil, nil, 46, + nil, 87, 37, 17, 94, nil, nil, nil, 12, 7, + 91, 103, 37, 27, 62, 21, 7, nil, 100, 97, + nil, nil, 106, nil, nil, nil, nil, nil ] + +racc_action_default = [ + -37, -13, -37, -19, -37, -20, -2, -4, -11, -6, + -12, -14, -7, -37, -37, -29, -28, -37, -37, -37, + -3, -23, -21, -22, -37, -5, -37, -37, -8, -29, + -37, -9, -27, -26, 68, -1, -37, -34, -35, -37, + -36, -37, -37, -37, -37, -15, -10, -25, -37, -37, + -37, -37, -37, -37, -37, -37, -37, -33, -37, -37, + -17, -18, -37, -24, -16, -32, -31, -30 ] + +racc_goto_table = [ + 4, 41, 20, 35, 17, 39, 25, nil, nil, nil, + nil, nil, nil, 27, nil, nil, 50, 30, nil, 54, + nil, nil, nil, nil, nil, 57, 59, nil, nil, 62, + nil, 20, nil, 65, 66, nil, nil, 67, nil, nil, + nil, nil, 52, 53, nil, nil, nil, nil, nil, 56 ] + +racc_goto_check = [ + 1, 10, 3, 2, 7, 9, 5, nil, nil, nil, + nil, nil, nil, 1, nil, nil, 10, 7, nil, 10, + nil, nil, nil, nil, nil, 10, 10, nil, nil, 10, + nil, 3, nil, 10, 10, nil, nil, 10, nil, nil, + nil, nil, 1, 1, nil, nil, nil, nil, nil, 1 ] + +racc_goto_pointer = [ + nil, 0, -16, -4, nil, -3, nil, 2, nil, -21, + -25 ] + +racc_goto_default = [ + nil, nil, 6, 7, 9, nil, 12, nil, 1, nil, + nil ] + +racc_reduce_table = [ + 0, 0, :racc_error, + 3, 26, :_reduce_1, + 1, 26, :_reduce_2, + 2, 27, :_reduce_3, + 1, 27, :_reduce_4, + 2, 28, :_reduce_5, + 1, 28, :_reduce_none, + 1, 29, :_reduce_none, + 3, 29, :_reduce_8, + 3, 29, :_reduce_9, + 4, 29, :_reduce_10, + 1, 29, :_reduce_11, + 1, 29, :_reduce_12, + 1, 29, :_reduce_13, + 1, 29, :_reduce_14, + 3, 31, :_reduce_15, + 6, 31, :_reduce_16, + 5, 31, :_reduce_17, + 5, 31, :_reduce_18, + 1, 33, :_reduce_none, + 1, 33, :_reduce_none, + 1, 30, :_reduce_none, + 1, 30, :_reduce_none, + 1, 30, :_reduce_none, + 5, 30, :_reduce_24, + 3, 30, :_reduce_25, + 2, 32, :_reduce_26, + 2, 32, :_reduce_27, + 1, 32, :_reduce_none, + 1, 32, :_reduce_none, + 4, 34, :_reduce_30, + 4, 34, :_reduce_31, + 4, 34, :_reduce_32, + 3, 34, :_reduce_33, + 1, 35, :_reduce_34, + 1, 35, :_reduce_35, + 1, 35, :_reduce_36 ] + +racc_reduce_n = 37 + +racc_shift_n = 68 + +racc_token_table = { + false => 0, + :error => 1, + :BAR => 2, + :LBRACK => 3, + :CTYPE => 4, + :RBRACK => 5, + :NEGATE => 6, + :CCLASS => 7, + :DOT => 8, + :CHAR => 9, + :LPAREN => 10, + :RPAREN => 11, + :QMARK => 12, + :COLON => 13, + :NAME => 14, + :L_ANCHOR => 15, + :R_ANCHOR => 16, + :STAR => 17, + :PLUS => 18, + :LCURLY => 19, + :RCURLY => 20, + :MINUS => 21, + :MULTILINE => 22, + :IGNORECASE => 23, + :EXTENDED => 24 } + +racc_nt_base = 25 + +racc_use_result_var = true + +Racc_arg = [ + racc_action_table, + racc_action_check, + racc_action_default, + racc_action_pointer, + racc_goto_table, + racc_goto_check, + racc_goto_default, + racc_goto_pointer, + racc_nt_base, + racc_reduce_table, + racc_token_table, + racc_shift_n, + racc_reduce_n, + racc_use_result_var ] + +Racc_token_to_s_table = [ + "$end", + "error", + "BAR", + "LBRACK", + "CTYPE", + "RBRACK", + "NEGATE", + "CCLASS", + "DOT", + "CHAR", + "LPAREN", + "RPAREN", + "QMARK", + "COLON", + "NAME", + "L_ANCHOR", + "R_ANCHOR", + "STAR", + "PLUS", + "LCURLY", + "RCURLY", + "MINUS", + "MULTILINE", + "IGNORECASE", + "EXTENDED", + "$start", + "expression", + "subexpression", + "quantified_atom", + "atom", + "quantifier", + "group", + "bracket_expression", + "anchor", + "options", + "modifier" ] + +Racc_debug_parser = false + +##### State transition tables end ##### + +# reduce 0 omitted + +def _reduce_1(val, _values, result) + # TODO remove this conditional by breaking + # it into another production + if val[0][0].is_a?(Regin::Alternation) + alt = val[0][0] + [Expression.new(val[2])] + else + alt = Alternation.new(val[0], Expression.new(val[2])) + end + result = Expression.new(alt) + + result +end + +def _reduce_2(val, _values, result) + result = Expression.new(val[0]) + result +end + +def _reduce_3(val, _values, result) + result = val[0] + [val[1]] + result +end + +def _reduce_4(val, _values, result) + result = [val[0]] + result +end + +def _reduce_5(val, _values, result) + result = val[0].dup(:quantifier => val[1]) + result +end + +# reduce 6 omitted + +# reduce 7 omitted + +def _reduce_8(val, _values, result) + result = CharacterClass.new(val[1]) + result +end + +def _reduce_9(val, _values, result) + result = CharacterClass.new(val[1]) + result +end + +def _reduce_10(val, _values, result) + result = CharacterClass.new(val[2], :negate => true) + result +end + +def _reduce_11(val, _values, result) + result = CharacterClass.new(val[0]) + result +end + +def _reduce_12(val, _values, result) + result = CharacterClass.new('.') + result +end + +def _reduce_13(val, _values, result) + result = Anchor.new(val[0]) + result +end + +def _reduce_14(val, _values, result) + result = Character.new(val[0]) + result +end + +def _reduce_15(val, _values, result) + result = Group.new(val[1], :index => @capture_index_stack.pop) + + result +end + +def _reduce_16(val, _values, result) + result = Group.new(val[4], val[2].merge(:capture => false)) + @options_stack.pop + + result +end + +def _reduce_17(val, _values, result) + result = Group.new(val[3], :capture => false); + + result +end + +def _reduce_18(val, _values, result) + result = Group.new(val[3], :name => val[2], :index => @capture_index_stack.pop); + + result +end + +# reduce 19 omitted + +# reduce 20 omitted + +# reduce 21 omitted + +# reduce 22 omitted + +# reduce 23 omitted + +def _reduce_24(val, _values, result) + result = val.join + result +end + +def _reduce_25(val, _values, result) + result = val.join + result +end + +def _reduce_26(val, _values, result) + result = val.join + result +end + +def _reduce_27(val, _values, result) + result = val.join + result +end + +# reduce 28 omitted + +# reduce 29 omitted + +def _reduce_30(val, _values, result) + @options_stack << result = { val[1] => false, val[2] => false, val[3] => false } + + result +end + +def _reduce_31(val, _values, result) + @options_stack << result = { val[0] => true, val[2] => false, val[3] => false } + + result +end + +def _reduce_32(val, _values, result) + @options_stack << result = { val[0] => true, val[1] => true, val[3] => false } + + result +end + +def _reduce_33(val, _values, result) + @options_stack << result = { val[0] => true, val[1] => true, val[2] => true } + + result +end + +def _reduce_34(val, _values, result) + result = :multiline + result +end + +def _reduce_35(val, _values, result) + result = :ignorecase + result +end + +def _reduce_36(val, _values, result) + result = :extended + result +end + +def _reduce_none(val, _values, result) + val[0] +end + + end # class Parser +end # module Regin + +require 'regin/tokenizer' diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/tokenizer.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/tokenizer.rb new file mode 100644 index 0000000000..59e4ffb611 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/tokenizer.rb @@ -0,0 +1,213 @@ +#-- +# DO NOT MODIFY!!!! +# This file is automatically generated by rex 1.0.5.beta1 +# from lexical definition file "lib/regin/tokenizer.rex". +#++ + +require 'racc/parser' +class Regin::Parser < Racc::Parser + require 'strscan' + + class ScanError < StandardError ; end + + attr_reader :lineno + attr_reader :filename + attr_accessor :state + + def scan_setup(str) + @ss = StringScanner.new(str) + @lineno = 1 + @state = nil + end + + def action + yield + end + + def scan_str(str) + scan_setup(str) + do_parse + end + alias :scan :scan_str + + def load_file( filename ) + @filename = filename + open(filename, "r") do |f| + scan_setup(f.read) + end + end + + def scan_file( filename ) + load_file(filename) + do_parse + end + + + def next_token + return if @ss.eos? + + text = @ss.peek(1) + @lineno += 1 if text == "\n" + token = case @state + when nil + case + when (text = @ss.scan(/\\[dDsSwW]/)) + action { [:CCLASS, text] } + + when (text = @ss.scan(/\^|\\A/)) + action { [:L_ANCHOR, text] } + + when (text = @ss.scan(/\$|\\Z/)) + action { [:R_ANCHOR, text] } + + when (text = @ss.scan(/<(\w+)>/)) + action { [:NAME, @ss[1]] } + + when (text = @ss.scan(/\(/)) + action { + @capture_index_stack << @capture_index + @capture_index += 1 + @state = :OPTIONS if @ss.peek(1) == '?'; + [:LPAREN, text] + } + + + when (text = @ss.scan(/\)/)) + action { [:RPAREN, text] } + + when (text = @ss.scan(/\[/)) + action { @state = :CCLASS; [:LBRACK, text] } + + when (text = @ss.scan(/\{/)) + action { [:LCURLY, text] } + + when (text = @ss.scan(/\}/)) + action { [:RCURLY, text] } + + when (text = @ss.scan(/\|/)) + action { [:BAR, text] } + + when (text = @ss.scan(/\./)) + action { [:DOT, text] } + + when (text = @ss.scan(/\?/)) + action { [:QMARK, text] } + + when (text = @ss.scan(/\+(?:\?)/)) + action { [:PLUS, text] } + + when (text = @ss.scan(/\*(?:\?)/)) + action { [:STAR, text] } + + when (text = @ss.scan(/\#/)) + action { + if @options_stack[-1][:extended] + @state = :COMMENT; + next_token + else + [:CHAR, text] + end + } + + + when (text = @ss.scan(/\s|\n/)) + action { + if @options_stack[-1][:extended] + next_token + else + [:CHAR, text] + end + } + + + when (text = @ss.scan(/\\(.)/)) + action { [:CHAR, @ss[1]] } + + when (text = @ss.scan(/./)) + action { [:CHAR, text] } + + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '" + text + "'" + end # if + + when :CCLASS + case + when (text = @ss.scan(/\]/)) + action { @state = nil; [:RBRACK, text] } + + when (text = @ss.scan(/\^/)) + action { [:NEGATE, text] } + + when (text = @ss.scan(/:(alnum|alpha|ascii|blank|cntrl|digit|graph|lower|print|punct|space|upper|word|xdigit):/)) + action { [:CTYPE, text] } + + when (text = @ss.scan(/\\-/)) + action { [:CHAR, text] } + + when (text = @ss.scan(/\\(.)/)) + action { [:CHAR, @ss[1]] } + + when (text = @ss.scan(/./)) + action { [:CHAR, text] } + + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '" + text + "'" + end # if + + when :OPTIONS + case + when (text = @ss.scan(/\?/)) + action { + @state = nil unless @ss.peek(1) =~ /-|m|i|x|:/ + [:QMARK, text] + } + + + when (text = @ss.scan(/\-/)) + action { [:MINUS, text] } + + when (text = @ss.scan(/m/)) + action { [:MULTILINE, text] } + + when (text = @ss.scan(/i/)) + action { [:IGNORECASE, text] } + + when (text = @ss.scan(/x/)) + action { [:EXTENDED, text] } + + when (text = @ss.scan(/\:/)) + action { + @capture_index_stack.pop + @capture_index -= 1 + @state = nil; + [:COLON, text] + } + + + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '" + text + "'" + end # if + + when :COMMENT + case + when (text = @ss.scan(/\n/)) + action { @state = nil; next_token } + + when (text = @ss.scan(/./)) + action { next_token } + + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '" + text + "'" + end # if + + else + raise ScanError, "undefined state: '" + state.to_s + "'" + end # case state + token + end # def next_token + +end # class diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/version.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/version.rb new file mode 100644 index 0000000000..7ad2a5a25e --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/version.rb @@ -0,0 +1,3 @@ +module Regin + Version = '0.3.3' +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/version.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/version.rb new file mode 100644 index 0000000000..a3688b102e --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/version.rb @@ -0,0 +1,5 @@ +module Rack + module Mount + Version = '0.6.6.pre' + end +end diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 4d06ca0d89..956c88a553 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -3,16 +3,19 @@ require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/array/wrap' require 'active_support/ordered_options' +require 'action_view/log_subscriber' module ActionView #:nodoc: class NonConcattingString < ActiveSupport::SafeBuffer end + # = Action View Base + # # Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> (or <tt>.rxml</tt>) extension then Jim Weirich's Builder::XmlMarkup library is used. # If the template file has a <tt>.rjs</tt> extension then it will use ActionView::Helpers::PrototypeHelper::JavaScriptGenerator. # - # = ERb + # == ERb # # You trigger ERb by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the # following loop for names: @@ -32,7 +35,7 @@ module ActionView #:nodoc: # # <%- and -%> suppress leading and trailing whitespace, including the trailing newline, and can be used interchangeably with <% and %>. # - # == Using sub templates + # === Using sub templates # # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts): @@ -54,7 +57,7 @@ module ActionView #:nodoc: # # <title><%= @page_title %></title> # - # == Passing local variables to sub templates + # === Passing local variables to sub templates # # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values: # @@ -74,7 +77,7 @@ module ActionView #:nodoc: # # Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction. # - # == Template caching + # === Template caching # # By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will # check the file's modification time and recompile it. @@ -202,8 +205,12 @@ module ActionView #:nodoc: value.dup : ActionView::PathSet.new(Array.wrap(value)) end + def assign(new_assigns) # :nodoc: + self.assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } + end + def initialize(lookup_context = nil, assigns_for_first_render = {}, controller = nil, formats = nil) #:nodoc: - self.assigns = assigns_for_first_render.each { |key, value| instance_variable_set("@#{key}", value) } + assign(assigns_for_first_render) self.helpers = self.class.helpers || Module.new if @_controller = controller diff --git a/actionpack/lib/action_view/context.rb b/actionpack/lib/action_view/context.rb index 88efd4b34f..39d88333e8 100644 --- a/actionpack/lib/action_view/context.rb +++ b/actionpack/lib/action_view/context.rb @@ -2,13 +2,12 @@ module ActionView module CompiledTemplates #:nodoc: # holds compiled template code end - - # Action View contexts are supplied to Action Controller - # to render template. The default Action View context - # is ActionView::Base. + # = Action View Context + # + # Action View contexts are supplied to Action Controller to render template. + # The default Action View context is ActionView::Base. # - # In order to work with ActionController, a Context - # must implement: + # In order to work with ActionController, a Context must implement: # # Context#render_partial[options] # - responsible for setting options[:_template] @@ -21,16 +20,14 @@ module ActionView # options<Hash>:: See _render_template_with_layout in ActionView::Base # partial<Boolean>:: Whether or not the template to render is a partial # - # An Action View context can also mix in Action View's - # helpers. In order to mix in helpers, a context must - # implement: + # An Action View context can also mix in Action View's helpers. In order to + # mix in helpers, a context must implement: # # Context#controller # - Returns an instance of AbstractController # - # In any case, a context must mix in ActionView::Context, - # which stores compiled template and provides the output - # buffer. + # In any case, a context must mix in ActionView::Context, which stores compiled + # template and provides the output buffer. module Context include CompiledTemplates attr_accessor :output_buffer diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index 8054de0af6..6bb0875bc3 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/object/blank' module ActionView + # = Active Model Helpers module Helpers module ActiveModelHelper %w(input form error_messages_for error_message_on).each do |method| @@ -35,12 +36,16 @@ module ActionView end end - %w(tag content_tag to_date_select_tag to_datetime_select_tag to_time_select_tag).each do |meth| + %w(content_tag to_date_select_tag to_datetime_select_tag to_time_select_tag).each do |meth| module_eval "def #{meth}(*) error_wrapping(super) end", __FILE__, __LINE__ end + def tag(type, options, *) + tag_generate_errors?(options) ? error_wrapping(super) : super + end + def error_wrapping(html_tag) - if object.respond_to?(:errors) && object.errors.respond_to?(:full_messages) && object.errors[@method_name].any? + if object_has_errors? Base.field_error_proc.call(html_tag, self) else html_tag @@ -50,6 +55,16 @@ module ActionView def error_message object.errors[@method_name] end + + private + + def object_has_errors? + object.respond_to?(:errors) && object.errors.respond_to?(:full_messages) && error_message.any? + end + + def tag_generate_errors?(options) + options['type'] != 'hidden' + end end class FormBuilder diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 25426a5547..a3c43d3e93 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -6,6 +6,7 @@ require 'active_support/core_ext/file' require 'active_support/core_ext/object/blank' module ActionView + # = Action View Asset Tag Helpers module Helpers #:nodoc: # This module provides methods for generating HTML that links views to assets such # as images, javascripts, stylesheets, and feeds. These methods do not verify @@ -193,7 +194,19 @@ module ActionView # RewriteEngine On # RewriteRule ^/release-\d+/(images|javascripts|stylesheets)/(.*)$ /$1/$2 [L] module AssetTagHelper - JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls', 'rails'].freeze unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES) + mattr_reader :javascript_expansions + @@javascript_expansions = { } + + mattr_reader :stylesheet_expansions + @@stylesheet_expansions = {} + + # You can enable or disable the asset tag timestamps cache. + # With the cache enabled, the asset tag helper methods will make fewer + # expensive file system calls. However this prevents you from modifying + # any asset files while the server is running. + # + # ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false + mattr_accessor :cache_asset_timestamps # 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 @@ -350,8 +363,6 @@ module ActionView end end - @@javascript_expansions = { :defaults => JAVASCRIPT_DEFAULT_SOURCES.dup } - # Register one or more javascript files to be included when <tt>symbol</tt> # is passed to <tt>javascript_include_tag</tt>. This method is typically intended # to be called from plugin initialization to register javascript files @@ -367,8 +378,6 @@ module ActionView @@javascript_expansions.merge!(expansions) end - @@stylesheet_expansions = {} - # Register one or more stylesheet files to be included when <tt>symbol</tt> # is passed to <tt>stylesheet_link_tag</tt>. This method is typically intended # to be called from plugin initialization to register stylesheet files @@ -384,18 +393,6 @@ module ActionView @@stylesheet_expansions.merge!(expansions) end - # Register one or more additional JavaScript files to be included when - # <tt>javascript_include_tag :defaults</tt> is called. This method is - # typically 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_expansions[:defaults].concat(sources) - end - - def self.reset_javascript_include_default #:nodoc: - @@javascript_expansions[:defaults] = JAVASCRIPT_DEFAULT_SOURCES.dup - end - # Computes the path to a stylesheet asset in the public stylesheets directory. # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs). # Full paths from the document root will be passed through. @@ -706,23 +703,8 @@ module ActionView tag("audio", options) end - def self.cache_asset_timestamps - @@cache_asset_timestamps - end - - # You can enable or disable the asset tag timestamps cache. - # With the cache enabled, the asset tag helper methods will make fewer - # expensive file system calls. However this prevents you from modifying - # any asset files while the server is running. - # - # ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false - def self.cache_asset_timestamps=(value) - @@cache_asset_timestamps = value - end - - @@cache_asset_timestamps = true - private + def rewrite_extension?(source, dir, ext) source_ext = File.extname(source)[1..-1] ext && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(config.assets_dir, dir, "#{source}.#{ext}")))) diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb index 52806f206a..cb5a1404ff 100644 --- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -1,10 +1,12 @@ require 'set' -# Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERb or any other -# template languages). module ActionView + # = Action View Atom Feed Helpers module Helpers #:nodoc: module AtomFeedHelper + # Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERb or any other + # template languages). + # # Full usage example: # # config/routes.rb: diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb index 8251ed18f4..f544a9d147 100644 --- a/actionpack/lib/action_view/helpers/cache_helper.rb +++ b/actionpack/lib/action_view/helpers/cache_helper.rb @@ -1,8 +1,10 @@ module ActionView + # = Action View Cache Helper module Helpers - # This helper to exposes a method for caching of view fragments. - # See ActionController::Caching::Fragments for usage instructions. module CacheHelper + # This helper to exposes a method for caching of view fragments. + # See ActionController::Caching::Fragments for usage instructions. + # # A method for caching fragments of a view rather than an entire # action or page. This technique is useful caching pieces like # menus, lists of news topics, static HTML fragments, and so on. diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index edc6afc622..f9105ca364 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -1,9 +1,11 @@ require 'active_support/core_ext/object/blank' module ActionView + # = Action View Capture Helper module Helpers # CaptureHelper exposes methods to let you extract generated markup which # can be used in other parts of a template or layout file. + # # It provides a method to capture blocks into variables through capture and # a way to capture a block of markup for use in a layout through content_for. module CaptureHelper @@ -176,9 +178,7 @@ module ActionView def flush_output_buffer #:nodoc: if output_buffer && !output_buffer.empty? response.body_parts << output_buffer - new = '' - new.force_encoding(output_buffer.encoding) if new.respond_to?(:force_encoding) - self.output_buffer = new + self.output_buffer = output_buffer[0,0] nil end end diff --git a/actionpack/lib/action_view/helpers/csrf_helper.rb b/actionpack/lib/action_view/helpers/csrf_helper.rb index 41c6b67f91..3d03f6aac6 100644 --- a/actionpack/lib/action_view/helpers/csrf_helper.rb +++ b/actionpack/lib/action_view/helpers/csrf_helper.rb @@ -1,7 +1,9 @@ module ActionView + # = Action View CSRF Helper module Helpers module CsrfHelper - # Returns a meta tag with the request forgery protection token for forms to use. Put this in your head. + # Returns a meta tag with the cross-site request forgery protection token + # for forms to use. Place this in your head. def csrf_meta_tag if protect_against_forgery? %(<meta name="csrf-param" content="#{Rack::Utils.escape_html(request_forgery_protection_token)}"/>\n<meta name="csrf-token" content="#{Rack::Utils.escape_html(form_authenticity_token)}"/>).html_safe diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 7d846a01dd..f097b9a5a3 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -4,6 +4,8 @@ require 'active_support/core_ext/hash/slice' module ActionView module Helpers + # = Action View Date Helpers + # # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the # select-type methods share a number of common options that are as follows: # @@ -580,10 +582,10 @@ module ActionView extend ActiveSupport::Memoizable include ActionView::Helpers::TagHelper - DEFAULT_PREFIX = 'date'.freeze unless const_defined?('DEFAULT_PREFIX') + DEFAULT_PREFIX = 'date'.freeze POSITION = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 - }.freeze unless const_defined?('POSITION') + }.freeze def initialize(datetime, options = {}, html_options = {}) @options = options.dup @@ -894,8 +896,10 @@ module ActionView # Returns the separator for a given datetime component def separator(type) case type - when :month, :day - @options[:date_separator] + when :month + @options[:discard_month] ? "" : @options[:date_separator] + when :day + @options[:discard_day] ? "" : @options[:date_separator] when :hour (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] when :minute diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb index e637dc1474..1491cb073f 100644 --- a/actionpack/lib/action_view/helpers/debug_helper.rb +++ b/actionpack/lib/action_view/helpers/debug_helper.rb @@ -1,6 +1,8 @@ module ActionView + # = Action View Debug Helper + # + # Provides a set of methods for making it easier to debug Rails objects. module Helpers - # Provides a set of methods for making it easier to debug Rails objects. module DebugHelper # Returns a YAML representation of +object+ wrapped with <pre> and </pre>. # If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead. diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index b3db3151d3..d1b10a9281 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -7,6 +7,7 @@ require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/blank' module ActionView + # = Action View Form Helpers module Helpers # Form helpers are designed to make working with resources much easier # compared to using vanilla HTML. @@ -92,7 +93,7 @@ module ActionView # # error handling # end # - # That's how you tipically work with resources. + # That's how you typically work with resources. module FormHelper extend ActiveSupport::Concern @@ -123,7 +124,6 @@ module ActionView # model: # # <%= form_for :person do |f| %> - # <%= f.error_messages %> # First name: <%= f.text_field :first_name %><br /> # Last name : <%= f.text_field :last_name %><br /> # Biography : <%= f.text_area :biography %><br /> @@ -220,15 +220,15 @@ module ActionView # <% end %> # # === Unobtrusive JavaScript - # - # Specifying: - # + # + # Specifying: + # # :remote => true # # in the options hash creates a form that will allow the unobtrusive JavaScript drivers to modify its - # behaviour. The expected default behaviour is an XMLHttpRequest in the background instead of the regular + # behaviour. The expected default behaviour is an XMLHttpRequest in the background instead of the regular # POST arrangement, but ultimately the behaviour is the choice of the JavaScript driver implementor. - # Even though it's using JavaScript to serialize the form elements, the form submission will work just like + # Even though it's using JavaScript to serialize the form elements, the form submission will work just like # a regular submission as viewed by the receiving side (all elements available in <tt>params</tt>). # # Example: @@ -269,7 +269,7 @@ module ActionView # <tt>labelling_form</tt>. # # The custom FormBuilder class is automatically merged with the options - # of a nested fields_for call, unless it's explicitely set. + # of a nested fields_for call, unless it's explicitly set. # # In many cases you will want to wrap the above in another helper, so you # could do something like the following: @@ -302,7 +302,7 @@ module ActionView args.unshift object end - options[:html][:remote] = true if options.delete(:remote) + (options[:html] ||= {})[:remote] = true if options.delete(:remote) output = form_tag(options.delete(:url) || {}, options.delete(:html) || {}) output << fields_for(object_name, *(args << options), &proc) @@ -717,7 +717,7 @@ module ActionView # # To prevent this the helper generates an auxiliary hidden field before # the very check box. The hidden field has the same name and its - # attributes mimick an unchecked check box. + # attributes mimic an unchecked check box. # # This way, the client either sends only the hidden field (representing # the check box is unchecked), or both fields. Since the HTML specification @@ -838,9 +838,9 @@ module ActionView attr_reader :method_name, :object_name - DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze unless const_defined?(:DEFAULT_FIELD_OPTIONS) - DEFAULT_RADIO_OPTIONS = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS) - DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS) + DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze + DEFAULT_RADIO_OPTIONS = { }.freeze + DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze def initialize(object_name, method_name, template_object, object = nil) @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup @@ -897,7 +897,7 @@ module ActionView options.delete("size") end options["type"] ||= field_type - options["value"] ||= value_before_type_cast(object) unless field_type == "file" + options["value"] = options.fetch("value"){ value_before_type_cast(object) } unless field_type == "file" options["value"] &&= html_escape(options["value"]) add_default_name_and_id(options) tag("input", options) @@ -1030,7 +1030,7 @@ module ActionView private def add_default_name_and_id_for_value(tag_value, options) unless tag_value.nil? - pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase + pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase specified_id = options["id"] add_default_name_and_id(options) options["id"] += "_#{pretty_tag_value}" if specified_id.blank? && options["id"].present? @@ -1042,14 +1042,14 @@ module ActionView def add_default_name_and_id(options) if options.has_key?("index") options["name"] ||= tag_name_with_index(options["index"]) - options["id"] = options.fetch("id", tag_id_with_index(options["index"])) + options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) } options.delete("index") elsif defined?(@auto_index) options["name"] ||= tag_name_with_index(@auto_index) - options["id"] = options.fetch("id", tag_id_with_index(@auto_index)) + options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) } else options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '') - options["id"] = options.fetch("id", tag_id) + options["id"] = options.fetch("id"){ tag_id } end end @@ -1180,7 +1180,7 @@ module ActionView # <%= form_for @post do |f| %> # <%= f.submit %> # <% end %> - # + # # In the example above, if @post is a new record, it will use "Create Post" as # submit button label, otherwise, it uses "Update Post". # diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index fe71d2cdf7..6f9d14de8b 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -4,6 +4,7 @@ require 'action_view/helpers/form_helper' require 'active_support/core_ext/object/blank' module ActionView + # = Action View Form Option Helpers module Helpers # Provides a number of methods for turning different kinds of containers into a set of option tags. # == Options @@ -398,7 +399,7 @@ module ActionView options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">" options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) options_for_select += '</optgroup>' - end + end.html_safe end # Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but @@ -412,8 +413,8 @@ module ActionView # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags, # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>. - # * +prompt+ - set to true or a prompt string. When the select element doesn’t have a value yet, this - # prepends an option with a generic prompt — "Please select" — or the given prompt string. + # * +prompt+ - set to true or a prompt string. When the select element doesn't have a value yet, this + # prepends an option with a generic prompt - "Please select" - or the given prompt string. # # Sample usage (Array): # grouped_options = [ diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 796268628a..4c1b751160 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/object/returning' require 'active_support/core_ext/object/blank' module ActionView + # = Action View Form Tag Helpers module Helpers # Provides a number of methods for creating form tags that doesn't rely on an Active Record object assigned to the template like # FormHelper does. Instead, you provide the names and values manually. @@ -528,23 +529,34 @@ module ActionView def html_options_for_form(url_for_options, options, *parameters_for_url) returning options.stringify_keys do |html_options| html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") + # The following URL is unescaped, this is just a hash of options, and it is the + # responsability of the caller to escape all the values. html_options["action"] = url_for(url_for_options, *parameters_for_url) + html_options["accept-charset"] = "UTF-8" html_options["data-remote"] = true if html_options.delete("remote") end end def extra_tags_for_form(html_options) - case method = html_options.delete("method").to_s - when /^get$/i # must be case-insentive, but can't use downcase as might be nil + snowman_tag = tag(:input, :type => "hidden", + :name => "_snowman", :value => "☃".html_safe) + + method = html_options.delete("method").to_s + + method_tag = case method + when /^get$/i # must be case-insensitive, but can't use downcase as might be nil html_options["method"] = "get" '' when /^post$/i, "", nil html_options["method"] = "post" - protect_against_forgery? ? content_tag(:div, token_tag, :style => 'margin:0;padding:0;display:inline') : '' + token_tag else html_options["method"] = "post" - content_tag(:div, tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag, :style => 'margin:0;padding:0;display:inline') + tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag end + + tags = snowman_tag << method_tag + content_tag(:div, tags, :style => 'margin:0;padding:0;display:inline') end def form_tag_html(html_options) diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index b0a7718f22..84f53b842c 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -1,6 +1,7 @@ require 'action_view/helpers/tag_helper' module ActionView + # = Action View JavaScript Helpers module Helpers # Provides functionality for working with JavaScript in your views. # diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 3b0dfb561c..37e5d91d8b 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/float/rounding' require 'active_support/core_ext/object/blank' module ActionView + # = Action View Number Helpers module Helpers #:nodoc: # Provides methods for converting numbers into formatted strings. @@ -332,7 +333,7 @@ module ActionView # number_to_human_size(483989, :precision => 2) # => 470 KB # number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,2 MB # - # Unsignificant zeros after the fractional separator are stripped out by default (set + # Non-significant zeros after the fractional separator are stripped out by default (set # <tt>:strip_insignificant_zeros</tt> to +false+ to change that): # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" # number_to_human_size(524288000, :precision=>5) # => "500 MB" diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index a798c3eaef..5c0ff5d59c 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -1,9 +1,9 @@ require 'set' require 'active_support/json' -require 'active_support/core_ext/object/returning' require 'active_support/core_ext/object/blank' module ActionView + # = Action View Prototype Helpers module Helpers # Prototype[http://www.prototypejs.org/] is a JavaScript library that provides # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation, @@ -94,14 +94,12 @@ module ActionView # See JavaScriptGenerator for information on updating multiple elements # on the page in an Ajax response. module PrototypeHelper - unless const_defined? :CALLBACKS - CALLBACKS = Set.new([ :create, :uninitialized, :loading, :loaded, - :interactive, :complete, :failure, :success ] + - (100..599).to_a) - AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url, - :asynchronous, :method, :insertion, :position, - :form, :with, :update, :script, :type ]).merge(CALLBACKS) - end + CALLBACKS = Set.new([ :create, :uninitialized, :loading, :loaded, + :interactive, :complete, :failure, :success ] + + (100..599).to_a) + AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url, + :asynchronous, :method, :insertion, :position, + :form, :with, :update, :script, :type ]).merge(CALLBACKS) # Returns the JavaScript needed for a remote function. # Takes the same arguments as link_to_remote. @@ -133,7 +131,7 @@ module ActionView url_options = options[:url] url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash) - function << "'#{escape_javascript(url_for(url_options))}'" + function << "'#{html_escape(escape_javascript(url_for(url_options)))}'" function << ", #{javascript_options})" function = "#{options[:before]}; #{function}" if options[:before] @@ -229,7 +227,7 @@ module ActionView # <script> tag. module GeneratorMethods def to_s #:nodoc: - returning javascript = @lines * $/ do + (@lines * $/).tap do |javascript| if ActionView::Base.debug_rjs source = javascript.dup javascript.replace "try {\n#{source}\n} catch (e) " @@ -531,9 +529,9 @@ module ActionView end def record(line) - returning line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" do - self << line - end + line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" + self << line + line end def render(*options) diff --git a/actionpack/lib/action_view/helpers/raw_output_helper.rb b/actionpack/lib/action_view/helpers/raw_output_helper.rb index 8c7f41177d..da7599fa8f 100644 --- a/actionpack/lib/action_view/helpers/raw_output_helper.rb +++ b/actionpack/lib/action_view/helpers/raw_output_helper.rb @@ -1,6 +1,15 @@ module ActionView #:nodoc: + # = Action View Raw Output Helper module Helpers #:nodoc: module RawOutputHelper + # This method outputs without escaping a string. Since escaping tags is + # now default, this can be used when you don't want Rails to automatically + # escape tags. This is not recommended if the data is coming from the user's + # input. + # + # For example: + # + # <%=raw @user.name %> def raw(stringish) stringish.to_s.html_safe end diff --git a/actionpack/lib/action_view/helpers/record_identification_helper.rb b/actionpack/lib/action_view/helpers/record_identification_helper.rb index 6c235bff3d..372f1cb8aa 100644 --- a/actionpack/lib/action_view/helpers/record_identification_helper.rb +++ b/actionpack/lib/action_view/helpers/record_identification_helper.rb @@ -1,4 +1,7 @@ module ActionView + # = Action View Record Identification Helpers + # + # See ActionController::RecordIdentifier for documentation on these methods. module Helpers module RecordIdentificationHelper # See ActionController::RecordIdentifier.partial_path -- this is just a delegate to that for convenient access in the view. diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb index a9cf15f418..7433f08777 100644 --- a/actionpack/lib/action_view/helpers/record_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -1,4 +1,5 @@ module ActionView + # = Action View Record Tag Helpers module Helpers module RecordTagHelper # Produces a wrapper DIV element with id and class parameters that diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb index f173523f6a..b47818a22a 100644 --- a/actionpack/lib/action_view/helpers/sanitize_helper.rb +++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb @@ -2,19 +2,25 @@ require 'action_controller/vendor/html-scanner' require 'action_view/helpers/tag_helper' module ActionView + # = Action View Sanitize Helpers module Helpers #:nodoc: # The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements. # These helper methods extend Action View making them callable within your template files. module SanitizeHelper - # This +sanitize+ helper will html encode all tags and strip all attributes that aren't specifically allowed. - # It also strips href/src tags with invalid protocols, like javascript: especially. It does its best to counter any - # tricks that hackers may use, like throwing in unicode/ascii/hex values to get past the javascript: filters. Check out + # This +sanitize+ helper will html encode all tags and strip all attributes that + # aren't specifically allowed. + # + # It also strips href/src tags with invalid protocols, like javascript: especially. + # It does its best to counter any tricks that hackers may use, like throwing in + # unicode/ascii/hex values to get past the javascript: filters. Check out # the extensive test suite. # # <%= sanitize @article.body %> # - # You can add or remove tags/attributes if you want to customize it a bit. See ActionView::Base for full docs on the - # available options. You can add tags/attributes for single uses of +sanitize+ by passing either the <tt>:attributes</tt> or <tt>:tags</tt> options: + # You can add or remove tags/attributes if you want to customize it a bit. + # See ActionView::Base for full docs on the available options. You can add + # tags/attributes for single uses of +sanitize+ by passing either the + # <tt>:attributes</tt> or <tt>:tags</tt> options: # # Normal Use # diff --git a/actionpack/lib/action_view/helpers/scriptaculous_helper.rb b/actionpack/lib/action_view/helpers/scriptaculous_helper.rb index 37319cca1b..8610c2469e 100644 --- a/actionpack/lib/action_view/helpers/scriptaculous_helper.rb +++ b/actionpack/lib/action_view/helpers/scriptaculous_helper.rb @@ -2,9 +2,11 @@ require 'action_view/helpers/javascript_helper' require 'active_support/json' module ActionView + # = Action View Scriptaculous Helpers module Helpers - # Provides a set of helpers for calling Scriptaculous JavaScript - # functions, including those which create Ajax controls and visual effects. + # Provides a set of helpers for calling Scriptaculous[http://script.aculo.us/] + # JavaScript functions, including those which create Ajax controls and visual + # effects. # # To be able to use these helpers, you must include the Prototype # JavaScript framework and the Scriptaculous JavaScript library in your @@ -12,12 +14,11 @@ module ActionView # for more information on including the necessary JavaScript. # # The Scriptaculous helpers' behavior can be tweaked with various options. + # # See the documentation at http://script.aculo.us for more information on # using these helpers in your application. module ScriptaculousHelper - unless const_defined? :TOGGLE_EFFECTS - TOGGLE_EFFECTS = [:toggle_appear, :toggle_slide, :toggle_blind] - end + TOGGLE_EFFECTS = [:toggle_appear, :toggle_slide, :toggle_blind] # Returns a JavaScript snippet to be used on the Ajax callbacks for # starting visual effects. diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index 66277f79fe..5d032b32a7 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/object/blank' require 'set' module ActionView + # = Action View Tag Helpers module Helpers #:nodoc: # Provides methods to generate HTML tags programmatically when you can't use # a Builder. By default, they output XHTML compliant tags. @@ -121,7 +122,7 @@ module ActionView attrs << %(#{key}="#{key}") if value elsif !value.nil? final_value = value.is_a?(Array) ? value.join(" ") : value - final_value = escape_once(final_value) if escape + final_value = html_escape(final_value) if escape attrs << %(#{key}="#{final_value}") end end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index c7f96597b9..0be8a2c36e 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/string/filters' require 'action_view/helpers/tag_helper' module ActionView + # = Action View Text Helpers module Helpers #:nodoc: # The TextHelper module provides a set of methods for filtering, formatting # and transforming strings, which can reduce the amount of inline Ruby code in @@ -52,7 +53,7 @@ module ActionView # truncate("Once upon a time in a world far far away", :length => 17) # # => "Once upon a ti..." # - # truncate("Once upon a time in a world far far away", :lenght => 17, :separator => ' ') + # truncate("Once upon a time in a world far far away", :length => 17, :separator => ' ') # # => "Once upon a..." # # truncate("And they found that many people were sleeping better.", :length => 25, :omission => '... (continued)') diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index 0d2b2aa7b1..dac9c28ab7 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -1,21 +1,28 @@ require 'action_view/helpers/tag_helper' module ActionView + # = Action View Translation Helpers module Helpers module TranslationHelper - # Delegates to I18n#translate but also performs three additional functions. First, it'll catch MissingTranslationData exceptions - # and turn them into inline spans that contains the missing key, such that you can see in a view what is missing where. + # Delegates to I18n#translate but also performs three additional functions. + # First, it'll catch MissingTranslationData exceptions and turn them into + # inline spans that contains the missing key, such that you can see in a + # view what is missing where. # - # Second, it'll scope the key by the current partial if the key starts with a period. So if you call translate(".foo") from the - # people/index.html.erb template, you'll actually be calling I18n.translate("people.index.foo"). This makes it less repetitive - # to translate many keys within the same partials and gives you a simple framework for scoping them consistently. If you don't - # prepend the key with a period, nothing is converted. + # Second, it'll scope the key by the current partial if the key starts + # with a period. So if you call <tt>translate(".foo")</tt> from the + # <tt>people/index.html.erb</tt> template, you'll actually be calling + # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive + # to translate many keys within the same partials and gives you a simple framework + # for scoping them consistently. If you don't prepend the key with a period, + # nothing is converted. # - # Third, it’ll mark the translation as safe HTML if the key has the suffix "_html" or the last element of the key is the word - # "html". For example, calling translate("footer_html") or translate("footer.html") will return a safe HTML string that won’t - # be escaped by other HTML helper methods. This naming convention helps to identify translations that include HTML tags so that + # Third, it'll mark the translation as safe HTML if the key has the suffix + # "_html" or the last element of the key is the word "html". For example, + # calling translate("footer_html") or translate("footer.html") will return + # a safe HTML string that won't be escaped by other HTML helper methods. This + # naming convention helps to identify translations that include HTML tags so that # you know what kind of output to expect when you call translate in a template. - def translate(key, options = {}) translation = I18n.translate(scope_key_by_partial(key), options.merge!(:raise => true)) if html_safe_translation_key?(key) && translation.respond_to?(:html_safe) diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 210f148c02..b8d6dc22f2 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/hash/keys' require 'action_dispatch' module ActionView + # = Action View URL Helpers module Helpers #:nodoc: # Provides a set of methods for making links and getting URLs that # depend on the routing subsystem (see ActionDispatch::Routing). @@ -12,7 +13,7 @@ module ActionView module UrlHelper # This helper may be included in any class that includes the # URL helpers of a router (router.url_helpers). Some methods - # provided here will only work in the context of a request + # provided here will only work in the4 context of a request # (link_to_unless_current, for instance), which must be provided # as a method called #request on the context. @@ -37,9 +38,6 @@ module ActionView # <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative "/controller/action" # instead of the fully qualified URL like "http://example.com/controller/action". # - # When called from a view, +url_for+ returns an HTML escaped url. If you - # need an unescaped url, pass <tt>:escape => false</tt> in the +options+. - # # ==== Options # * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path. # * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified). @@ -49,7 +47,6 @@ module ActionView # * <tt>:protocol</tt> - Overrides the default (current) protocol if provided. # * <tt>:user</tt> - Inline HTTP authentication (only plucked out if <tt>:password</tt> is also present). # * <tt>:password</tt> - Inline HTTP authentication (only plucked out if <tt>:user</tt> is also present). - # * <tt>:escape</tt> - Determines whether the returned URL will be HTML escaped or not (<tt>true</tt> by default). # # ==== Relying on named routes # @@ -71,10 +68,7 @@ module ActionView # <%= url_for(:action => 'play', :anchor => 'player') %> # # => /messages/play/#player # - # <%= url_for(:action => 'checkout', :anchor => 'tax&ship') %> - # # => /testing/jump/#tax&ship - # - # <%= url_for(:action => 'checkout', :anchor => 'tax&ship', :escape => false) %> + # <%= url_for(:action => 'jump', :anchor => 'tax&ship') %> # # => /testing/jump/#tax&ship # # <%= url_for(Workshop.new) %> @@ -99,21 +93,17 @@ module ActionView options ||= {} url = case options when String - escape = true options when Hash options = { :only_path => options[:host].nil? }.update(options.symbolize_keys) - escape = options.key?(:escape) ? options.delete(:escape) : false super when :back - escape = false controller.request.env["HTTP_REFERER"] || 'javascript:history.back()' else - escape = false polymorphic_path(options) end - escape ? escape_once(url).html_safe : url + url end # Creates a link tag of the given +name+ using a URL created by the set @@ -253,8 +243,8 @@ module ActionView tag_options = nil end - href_attr = "href=\"#{url}\"" unless href - "<a #{href_attr}#{tag_options}>#{ERB::Util.h(name || url)}</a>".html_safe + href_attr = "href=\"#{html_escape(url)}\"" unless href + "<a #{href_attr}#{tag_options}>#{html_escape(name || url)}</a>".html_safe end end @@ -338,7 +328,7 @@ module ActionView html_options.merge!("type" => "submit", "value" => name) - ("<form method=\"#{form_method}\" action=\"#{escape_once url}\" #{"data-remote=\"true\"" if remote} class=\"button_to\"><div>" + + ("<form method=\"#{form_method}\" action=\"#{html_escape(url)}\" #{"data-remote=\"true\"" if remote} class=\"button_to\"><div>" + method_tag + tag("input", html_options) + request_token_tag + "</div></form>").html_safe end @@ -484,24 +474,27 @@ module ActionView # :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 = {}) + email_address = html_escape(email_address) + html_options = html_options.stringify_keys encode = html_options.delete("encode").to_s cc, bcc, subject, body = html_options.delete("cc"), html_options.delete("bcc"), html_options.delete("subject"), html_options.delete("body") - string = '' - extras = '' - extras << "cc=#{Rack::Utils.escape(cc).gsub("+", "%20")}&" unless cc.nil? - extras << "bcc=#{Rack::Utils.escape(bcc).gsub("+", "%20")}&" unless bcc.nil? - extras << "body=#{Rack::Utils.escape(body).gsub("+", "%20")}&" unless body.nil? - extras << "subject=#{Rack::Utils.escape(subject).gsub("+", "%20")}&" unless subject.nil? - extras = "?" << extras.gsub!(/&?$/,"") unless extras.empty? - - email_address_obfuscated = html_escape(email_address) + extras = [] + extras << "cc=#{Rack::Utils.escape(cc).gsub("+", "%20")}" unless cc.nil? + extras << "bcc=#{Rack::Utils.escape(bcc).gsub("+", "%20")}" unless bcc.nil? + extras << "body=#{Rack::Utils.escape(body).gsub("+", "%20")}" unless body.nil? + extras << "subject=#{Rack::Utils.escape(subject).gsub("+", "%20")}" unless subject.nil? + extras = extras.empty? ? '' : '?' + html_escape(extras.join('&')) + + 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") + string = '' + if encode == "javascript" - "document.write('#{content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');".each_byte do |c| + "document.write('#{content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe))}');".each_byte do |c| string << sprintf("%%%x", c) end "<script type=\"#{Mime::JS}\">eval(decodeURIComponent('#{string}'))</script>".html_safe @@ -518,9 +511,9 @@ module ActionView char = c.chr string << (char =~ /\w/ ? sprintf("%%%x", c) : char) end - content_tag "a", name || email_address_encoded.html_safe, html_options.merge({ "href" => "#{string}#{extras}" }) + content_tag "a", name || email_address_encoded.html_safe, html_options.merge("href" => "#{string}#{extras}".html_safe) else - content_tag "a", name || email_address_obfuscated.html_safe, html_options.merge({ "href" => "mailto:#{email_address}#{extras}" }) + content_tag "a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe) end end @@ -573,7 +566,7 @@ module ActionView "in a #request method" end - url_string = CGI.unescapeHTML(url_for(options)) + url_string = url_for(options) # We ignore any extra parameters in the request_uri if the # submitted url doesn't have any either. This lets the function diff --git a/actionpack/lib/action_view/log_subscriber.rb b/actionpack/lib/action_view/log_subscriber.rb new file mode 100644 index 0000000000..443a0eafd1 --- /dev/null +++ b/actionpack/lib/action_view/log_subscriber.rb @@ -0,0 +1,28 @@ +module ActionView + # = Action View Log Subscriber + # + # Provides functionality so that Rails can output logs from Action View. + class LogSubscriber < ActiveSupport::LogSubscriber + def render_template(event) + message = "Rendered #{from_rails_root(event.payload[:identifier])}" + message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] + message << (" (%.1fms)" % event.duration) + info(message) + end + alias :render_partial :render_template + alias :render_collection :render_template + + # TODO: Ideally, ActionView should have its own logger so it does not depend on AC.logger + def logger + ActionController::Base.logger if defined?(ActionController::Base) + end + + protected + + def from_rails_root(string) + string.sub("#{Rails.root}/", "").sub(/^app\/views\//, "") + end + end +end + +ActionView::LogSubscriber.attach_to :action_view diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index 823226cb9c..3ea8b86af1 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -2,6 +2,8 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' module ActionView + # = Action View Lookup Context + # # LookupContext is the object responsible to hold all information required to lookup # templates, i.e. view paths and details. The LookupContext is also responsible to # generate a key, given to view paths, used in the resolver cache lookup. Since diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb index 7f5e5d11b8..9857d688d2 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/paths.rb @@ -1,4 +1,5 @@ module ActionView #:nodoc: + # = Action View PathSet class PathSet < Array #:nodoc: %w(initialize << concat insert push unshift).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 diff --git a/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb index c606a71e18..33dfcbb803 100644 --- a/actionpack/lib/action_view/railtie.rb +++ b/actionpack/lib/action_view/railtie.rb @@ -2,11 +2,11 @@ require "action_view" require "rails" module ActionView + # = Action View Railtie class Railtie < Rails::Railtie config.action_view = ActiveSupport::OrderedOptions.new - - require "action_view/railties/log_subscriber" - log_subscriber :action_view, ActionView::Railties::LogSubscriber.new + config.action_view.stylesheet_expansions = {} + config.action_view.javascript_expansions = { :defaults => ['prototype', 'effects', 'dragdrop', 'controls', 'rails'] } initializer "action_view.cache_asset_timestamps" do |app| unless app.config.cache_classes @@ -16,6 +16,18 @@ module ActionView end end + initializer "action_view.javascript_expansions" do |app| + ActiveSupport.on_load(:action_view) do + ActionView::Helpers::AssetTagHelper.register_javascript_expansion( + app.config.action_view.delete(:javascript_expansions) + ) + + ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion( + app.config.action_view.delete(:stylesheet_expansions) + ) + end + end + initializer "action_view.set_configs" do |app| ActiveSupport.on_load(:action_view) do app.config.action_view.each do |k,v| diff --git a/actionpack/lib/action_view/railties/log_subscriber.rb b/actionpack/lib/action_view/railties/log_subscriber.rb deleted file mode 100644 index 9487a10706..0000000000 --- a/actionpack/lib/action_view/railties/log_subscriber.rb +++ /dev/null @@ -1,24 +0,0 @@ -module ActionView - module Railties - class LogSubscriber < Rails::LogSubscriber - def render_template(event) - message = "Rendered #{from_rails_root(event.payload[:identifier])}" - message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << (" (%.1fms)" % event.duration) - info(message) - end - alias :render_partial :render_template - alias :render_collection :render_template - - def logger - ActionController::Base.logger - end - - protected - - def from_rails_root(string) - string.sub("#{Rails.root}/", "").sub(/^app\/views\//, "") - end - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_view/render/layouts.rb b/actionpack/lib/action_view/render/layouts.rb index a9dfc0cced..a474783a20 100644 --- a/actionpack/lib/action_view/render/layouts.rb +++ b/actionpack/lib/action_view/render/layouts.rb @@ -1,4 +1,5 @@ module ActionView + # = Action View Layouts module Layouts # Returns the contents that are yielded to a layout, given a name or a block. # @@ -55,7 +56,7 @@ module ActionView end # This is the method which actually finds the layout using details in the lookup - # context object. If no layout is found, it checkes if at least a layout with + # context object. If no layout is found, it checks if at least a layout with # the given name exists across all details before raising the error. def find_layout(layout) begin diff --git a/actionpack/lib/action_view/render/partials.rb b/actionpack/lib/action_view/render/partials.rb index 85f67d4f14..459aae94a2 100644 --- a/actionpack/lib/action_view/render/partials.rb +++ b/actionpack/lib/action_view/render/partials.rb @@ -1,6 +1,8 @@ require 'active_support/core_ext/object/blank' module ActionView + # = Action View Partials + # # There's also a convenience method for rendering sub templates within the current controller that depends on a # single object (we call this kind of sub templates for partials). It relies on the fact that partials should # follow the naming convention of being prefixed with an underscore -- as to separate them from regular @@ -316,7 +318,7 @@ module ActionView object.class.model_name.partial_path.dup.tap do |partial| path = @view.controller_path - partial.insert(0, "#{File.dirname(path)}/") if path.include?(?/) + partial.insert(0, "#{File.dirname(path)}/") if partial.include?(?/) && path.include?(?/) end end end diff --git a/actionpack/lib/action_view/render/rendering.rb b/actionpack/lib/action_view/render/rendering.rb index 4d35296932..5320245173 100644 --- a/actionpack/lib/action_view/render/rendering.rb +++ b/actionpack/lib/action_view/render/rendering.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/object/try' module ActionView + # = Action View Rendering module Rendering # Returns the result of a render that's dictated by the options hash. The primary options are: # diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 53ad24fdc6..40ff1f2182 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/kernel/singleton_class' module ActionView + # = Action View Template class Template extend ActiveSupport::Autoload @@ -155,11 +156,12 @@ module ActionView end def inspect - if defined?(Rails.root) - identifier.sub("#{Rails.root}/", '') - else - identifier - end + @inspect ||= + if defined?(Rails.root) + identifier.sub("#{Rails.root}/", '') + else + identifier + end end private @@ -266,9 +268,11 @@ module ActionView end def build_method_name(locals) - # TODO: is locals.keys.hash reliably the same? - @method_names[locals.keys.hash] ||= - "_render_template_#{@identifier.hash}_#{__id__}_#{locals.keys.hash}".gsub('-', "_") + @method_names[locals.keys.hash] ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}_#{locals.keys.hash}".gsub('-', "_") + end + + def identifier_method_name + @identifier_method_name ||= inspect.gsub(/[^a-z_]/, '_') end end end diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb index e50de7e5af..b1839b65e5 100644 --- a/actionpack/lib/action_view/template/error.rb +++ b/actionpack/lib/action_view/template/error.rb @@ -1,6 +1,7 @@ require "active_support/core_ext/enumerable" module ActionView + # = Action View Errors class ActionViewError < StandardError #:nodoc: end diff --git a/actionpack/lib/action_view/template/handlers.rb b/actionpack/lib/action_view/template/handlers.rb index 6228d7ac39..84d6474dd1 100644 --- a/actionpack/lib/action_view/template/handlers.rb +++ b/actionpack/lib/action_view/template/handlers.rb @@ -1,4 +1,5 @@ module ActionView #:nodoc: + # = Action View Template Handlers class Template module Handlers #:nodoc: autoload :ERB, 'action_view/template/handlers/erb' diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index ef44925951..c9e20ca14e 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -3,6 +3,7 @@ require "active_support/core_ext/class" require "action_view/template" module ActionView + # = Action View Resolver class Resolver def initialize @cached = Hash.new { |h1,k1| h1[k1] = @@ -98,7 +99,7 @@ module ActionView def initialize(path) raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver) super() - @path = Pathname.new(path).expand_path + @path = File.expand_path(path) end def eql?(resolver) diff --git a/actionpack/lib/action_view/template/text.rb b/actionpack/lib/action_view/template/text.rb index 269340514c..51be831dfb 100644 --- a/actionpack/lib/action_view/template/text.rb +++ b/actionpack/lib/action_view/template/text.rb @@ -1,4 +1,5 @@ module ActionView #:nodoc: + # = Action View Text Template class Template class Text < String #:nodoc: attr_accessor :mime_type diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 15d424be74..e5614c9e3b 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -4,6 +4,7 @@ require 'action_controller/test_case' require 'action_view' module ActionView + # = Action View Test Case class TestCase < ActiveSupport::TestCase class TestController < ActionController::Base include ActionDispatch::TestProcess @@ -84,6 +85,7 @@ module ActionView def setup_with_controller @controller = ActionView::TestCase::TestController.new + @request = @controller.request @output_buffer = ActiveSupport::SafeBuffer.new @rendered = '' @@ -97,10 +99,15 @@ module ActionView end def render(options = {}, local_assigns = {}, &block) - @rendered << output = _view.render(options, local_assigns, &block) + view.assign(_assigns) + @rendered << output = view.render(options, local_assigns, &block) output end + def locals + @locals ||= {} + end + included do setup :setup_with_controller end @@ -130,36 +137,51 @@ module ActionView end end - def _view - @_view ||= begin - view = ActionView::Base.new(ActionController::Base.view_paths, _assigns, @controller) + module Locals + attr_accessor :locals + + def _render_partial(options) + locals[options[:partial]] = options[:locals] + super(options) + end + end + + # The instance of ActionView::Base that is used by +render+. + def view + @view ||= begin + view = ActionView::Base.new(ActionController::Base.view_paths, {}, @controller) view.singleton_class.send :include, _helpers view.singleton_class.send :include, @controller._router.url_helpers view.singleton_class.send :delegate, :alert, :notice, :to => "request.flash" + view.extend(Locals) + view.locals = self.locals view.output_buffer = self.output_buffer view end end + alias_method :_view, :view + EXCLUDE_IVARS = %w{ + @_assertion_wrapped @_result + @controller + @layouts + @locals + @method_name @output_buffer + @partials @rendered + @request + @routes @templates - @view_context_class - @layouts - @partials - @controller - - @method_name - @fixture_cache - @loaded_fixtures @test_passed + @view + @view_context_class } def _instance_variables - instance_variables - EXCLUDE_IVARS - instance_variables + instance_variables.map(&:to_s) - EXCLUDE_IVARS end def _assigns diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index 0ce1dc506b..232a1679e0 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -47,8 +47,12 @@ module AbstractController end def index - self.response_body = @text - end + self.response_body = @text.to_s + end + end + + class Callback2Overwrite < Callback2 + before_filter :first, :except => :index end class TestCallbacks2 < ActiveSupport::TestCase @@ -70,6 +74,12 @@ module AbstractController @controller.process(:index) assert_equal "FIRSTSECOND", @controller.instance_variable_get("@aroundz") end + + test "before_filter with overwritten condition" do + @controller = Callback2Overwrite.new + result = @controller.process(:index) + assert_equal "", @controller.response_body + end end class Callback3 < ControllerWithCallbacks diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 84c5395610..c8477fb83f 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -38,6 +38,17 @@ end require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late +module Rails +end + +# Monkey patch the old router initialization to be silenced. +class ActionDispatch::Routing::DeprecatedMapper + def initialize_with_silencer(*args) + ActiveSupport::Deprecation.silence { initialize_without_silencer(*args) } + end + alias_method_chain :initialize, :silencer +end + ActiveSupport::Dependencies.hook! # Show backtraces for deprecated behavior for quicker cleanup. @@ -202,6 +213,21 @@ class ActionController::IntegrationTest < ActiveSupport::TestCase self.class.app = old_app silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) } end + + def with_autoload_path(path) + path = File.join(File.dirname(__FILE__), "fixtures", path) + if ActiveSupport::Dependencies.autoload_paths.include?(path) + yield + else + begin + ActiveSupport::Dependencies.autoload_paths << path + yield + ensure + ActiveSupport::Dependencies.autoload_paths.reject! {|p| p == path} + ActiveSupport::Dependencies.clear + end + end + end end # Temporary base class diff --git a/actionpack/test/activerecord/active_record_store_test.rb b/actionpack/test/activerecord/active_record_store_test.rb index 6d4b8e1e40..bdd1a0a15c 100644 --- a/actionpack/test/activerecord/active_record_store_test.rb +++ b/actionpack/test/activerecord/active_record_store_test.rb @@ -17,7 +17,6 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest end def get_session_id - session[:foo] render :text => "#{request.session_options[:id]}" end @@ -58,6 +57,10 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest get '/get_session_value' assert_response :success assert_equal 'foo: "baz"', response.body + + get '/call_reset_session' + assert_response :success + assert_not_equal [], headers['Set-Cookie'] end end end @@ -92,6 +95,34 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest end end + def test_getting_session_value_after_session_reset + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + session_cookie = cookies.send(:hash_for)['_session_id'] + + get '/call_reset_session' + assert_response :success + assert_not_equal [], headers['Set-Cookie'] + + cookies << session_cookie # replace our new session_id with our old, pre-reset session_id + + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from the database" + end + end + + def test_getting_from_nonexistent_session + with_test_route_set do + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body + assert_nil cookies['_session_id'], "should only create session on write, not read" + end + end + def test_getting_session_id with_test_route_set do get '/set_session_value' @@ -101,7 +132,19 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest get '/get_session_id' assert_response :success - assert_equal session_id, response.body + assert_equal session_id, response.body, "should be able to read session id without accessing the session hash" + end + end + + def test_doesnt_write_session_cookie_if_session_id_is_already_exists + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + + get '/get_session_value' + assert_response :success + assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists" end end diff --git a/actionpack/test/activerecord/controller_runtime_test.rb b/actionpack/test/activerecord/controller_runtime_test.rb index 331f861d8f..cfd86d704d 100644 --- a/actionpack/test/activerecord/controller_runtime_test.rb +++ b/actionpack/test/activerecord/controller_runtime_test.rb @@ -1,8 +1,8 @@ require 'active_record_unit' require 'active_record/railties/controller_runtime' require 'fixtures/project' -require 'rails/log_subscriber/test_helper' -require 'action_controller/railties/log_subscriber' +require 'active_support/log_subscriber/test_helper' +require 'action_controller/log_subscriber' ActionController::Base.send :include, ActiveRecord::Railties::ControllerRuntime @@ -13,18 +13,18 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase end end - include Rails::LogSubscriber::TestHelper + include ActiveSupport::LogSubscriber::TestHelper tests LogSubscriberController def setup super @old_logger = ActionController::Base.logger - Rails::LogSubscriber.add(:action_controller, ActionController::Railties::LogSubscriber.new) + ActionController::LogSubscriber.attach_to :action_controller end def teardown super - Rails::LogSubscriber.log_subscribers.clear + ActiveSupport::LogSubscriber.log_subscribers.clear ActionController::Base.logger = @old_logger end diff --git a/actionpack/test/activerecord/polymorphic_routes_test.rb b/actionpack/test/activerecord/polymorphic_routes_test.rb index 9f5e8ec657..90a1ef982c 100644 --- a/actionpack/test/activerecord/polymorphic_routes_test.rb +++ b/actionpack/test/activerecord/polymorphic_routes_test.rb @@ -381,6 +381,7 @@ class PolymorphicRoutesTest < ActionController::TestCase with_test_routes do @series.save assert_equal "http://example.com/series/#{@series.id}", polymorphic_url(@series) + assert_equal "http://example.com/series", polymorphic_url(Series.new) end end @@ -407,18 +408,18 @@ class PolymorphicRoutesTest < ActionController::TestCase def with_admin_test_routes(options = {}) with_routing do |set| - set.draw do |map| - map.namespace :admin do |admin| - admin.resources :projects do |projects| - projects.resources :tasks - projects.resource :bid do |bid| - bid.resources :tasks + set.draw do + namespace :admin do + resources :projects do + resources :tasks + resource :bid do + resources :tasks end end - admin.resources :taxes do |taxes| - taxes.resources :faxes + resources :taxes do + resources :faxes end - admin.resources :series + resources :series end end @@ -429,12 +430,12 @@ class PolymorphicRoutesTest < ActionController::TestCase def with_admin_and_site_test_routes(options = {}) with_routing do |set| - set.draw do |map| - map.namespace :admin do |admin| - admin.resources :projects do |projects| - projects.namespace :site do |site| - site.resources :tasks do |tasks| - tasks.resources :steps + set.draw do + namespace :admin do + resources :projects do + namespace :site do + resources :tasks do + resources :steps end end end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index 4f58b5d968..ae270b751e 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -2,9 +2,6 @@ require 'abstract_unit' require 'logger' require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late -module Rails -end - # Provide some controller to run the tests on. module Submodule class ContainedEmptyController < ActionController::Base diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 5c636cbab8..4be09f8c83 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -236,6 +236,15 @@ class FlashIntegrationTest < ActionController::IntegrationTest end end + def test_just_using_flash_does_not_stream_a_cookie_back + with_test_route_set do + get '/use_flash' + assert_response :success + assert_nil @response.headers["Set-Cookie"] + assert_equal "flash: ", @response.body + end + end + private # Overwrite get to send SessionSecret in env hash @@ -247,10 +256,15 @@ class FlashIntegrationTest < ActionController::IntegrationTest def with_test_route_set with_routing do |set| set.draw do |map| - match ':action', :to => ActionDispatch::Session::CookieStore.new( - FlashIntegrationTest::TestController, :key => FlashIntegrationTest::SessionKey, :secret => FlashIntegrationTest::SessionSecret - ) + match ':action', :to => FlashIntegrationTest::TestController + end + + @app = self.class.build_app(set) do |middleware| + middleware.use ActionDispatch::Session::CookieStore, :key => SessionKey + middleware.use ActionDispatch::Flash + middleware.delete "ActionDispatch::ShowExceptions" end + yield end end diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index b11eba2f89..0a18741f0c 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -1,6 +1,6 @@ require "abstract_unit" -require "rails/log_subscriber/test_helper" -require "action_controller/railties/log_subscriber" +require "active_support/log_subscriber/test_helper" +require "action_controller/log_subscriber" module Another class LogSubscribersController < ActionController::Base @@ -37,7 +37,7 @@ end class ACLogSubscriberTest < ActionController::TestCase tests Another::LogSubscribersController - include Rails::LogSubscriber::TestHelper + include ActiveSupport::LogSubscriber::TestHelper def setup super @@ -47,12 +47,12 @@ class ACLogSubscriberTest < ActionController::TestCase @cache_path = File.expand_path('../temp/test_cache', File.dirname(__FILE__)) ActionController::Base.page_cache_directory = @cache_path @controller.cache_store = :file_store, @cache_path - Rails::LogSubscriber.add(:action_controller, ActionController::Railties::LogSubscriber.new) + ActionController::LogSubscriber.attach_to :action_controller end def teardown super - Rails::LogSubscriber.log_subscribers.clear + ActiveSupport::LogSubscriber.log_subscribers.clear FileUtils.rm_rf(@cache_path) ActionController::Base.logger = @old_logger end diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb index 0b40f8ce95..8fa5d20372 100644 --- a/actionpack/test/controller/new_base/base_test.rb +++ b/actionpack/test/controller/new_base/base_test.rb @@ -31,9 +31,17 @@ module Dispatching end class EmptyController < ActionController::Base ; end + class SubEmptyController < EmptyController ; end + class NonDefaultPathController < ActionController::Base + def self.controller_path; "i_am_not_default"; end + end module Submodule class ContainedEmptyController < ActionController::Base ; end + class ContainedSubEmptyController < ContainedEmptyController ; end + class ContainedNonDefaultPathController < ActionController::Base + def self.controller_path; "i_am_extremly_not_default"; end + end end class BaseTest < Rack::TestCase @@ -65,16 +73,46 @@ module Dispatching assert_equal EmptyController.controller_path, EmptyController.new.controller_path end + test "non-default controller path" do + assert_equal 'i_am_not_default', NonDefaultPathController.controller_path + assert_equal NonDefaultPathController.controller_path, NonDefaultPathController.new.controller_path + end + + test "sub controller path" do + assert_equal 'dispatching/sub_empty', SubEmptyController.controller_path + assert_equal SubEmptyController.controller_path, SubEmptyController.new.controller_path + end + test "namespaced controller path" do assert_equal 'dispatching/submodule/contained_empty', Submodule::ContainedEmptyController.controller_path assert_equal Submodule::ContainedEmptyController.controller_path, Submodule::ContainedEmptyController.new.controller_path end + test "namespaced non-default controller path" do + assert_equal 'i_am_extremly_not_default', Submodule::ContainedNonDefaultPathController.controller_path + assert_equal Submodule::ContainedNonDefaultPathController.controller_path, Submodule::ContainedNonDefaultPathController.new.controller_path + end + + test "namespaced sub controller path" do + assert_equal 'dispatching/submodule/contained_sub_empty', Submodule::ContainedSubEmptyController.controller_path + assert_equal Submodule::ContainedSubEmptyController.controller_path, Submodule::ContainedSubEmptyController.new.controller_path + end + test "controller name" do assert_equal 'empty', EmptyController.controller_name assert_equal 'contained_empty', Submodule::ContainedEmptyController.controller_name end + test "non-default path controller name" do + assert_equal 'non_default_path', NonDefaultPathController.controller_name + assert_equal 'contained_non_default_path', Submodule::ContainedNonDefaultPathController.controller_name + end + + test "sub controller name" do + assert_equal 'sub_empty', SubEmptyController.controller_name + assert_equal 'contained_sub_empty', Submodule::ContainedSubEmptyController.controller_name + end + test "action methods" do assert_equal Set.new(%w( index diff --git a/actionpack/test/controller/record_identifier_test.rb b/actionpack/test/controller/record_identifier_test.rb index 813dedc80d..6a84475758 100644 --- a/actionpack/test/controller/record_identifier_test.rb +++ b/actionpack/test/controller/record_identifier_test.rb @@ -13,6 +13,19 @@ class Comment end end +class Sheep + extend ActiveModel::Naming + include ActiveModel::Conversion + + attr_reader :id + def to_key; id ? [id] : nil end + def save; @id = 1 end + def new_record?; @id.nil? end + def name + @id.nil? ? 'new sheep' : "sheep ##{@id}" + end +end + class Comment::Nested < Comment; end class Test::Unit::TestCase @@ -20,7 +33,7 @@ class Test::Unit::TestCase def comments_url 'http://www.example.com/comments' end - + def comment_url(comment) "http://www.example.com/comments/#{comment.id}" end @@ -35,6 +48,7 @@ class RecordIdentifierTest < Test::Unit::TestCase @record = @klass.new @singular = 'comment' @plural = 'comments' + @uncountable = Sheep end def test_dom_id_with_new_record @@ -58,7 +72,7 @@ class RecordIdentifierTest < Test::Unit::TestCase def test_dom_class assert_equal @singular, dom_class(@record) end - + def test_dom_class_with_prefix assert_equal "custom_prefix_#{@singular}", dom_class(@record, :custom_prefix) end @@ -79,6 +93,11 @@ class RecordIdentifierTest < Test::Unit::TestCase assert_equal @plural, plural_class_name(@klass) end + def test_uncountable + assert_equal true, uncountable?(@uncountable) + assert_equal false, uncountable?(@klass) + end + private def method_missing(method, *args) RecordIdentifier.send(method, *args) diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index e3ed097c67..a57a12f271 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -7,6 +7,10 @@ module Fun # :ported: def hello_world end + + def nested_partial_with_form_builder + render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {}, Proc.new {}) + end end end @@ -1230,6 +1234,13 @@ class RenderTest < ActionController::TestCase assert_match(/<label/, @response.body) assert_template('test/_labelling_form') end + + def test_nested_partial_with_form_builder + @controller = Fun::GamesController.new + get :nested_partial_with_form_builder + assert_match(/<label/, @response.body) + assert_template('fun/games/_form') + end def test_partial_collection get :partial_collection diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 907acf9573..4b30b0af36 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -34,9 +34,15 @@ module AbstractController ) end - def test_anchor_should_be_cgi_escaped - assert_equal('/c/a#anc%2Fhor', - W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('anc/hor')) + def test_anchor_should_escape_unsafe_pchar + assert_equal('/c/a#%23anchor', + W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('#anchor')) + ) + end + + def test_anchor_should_not_escape_safe_pchar + assert_equal('/c/a#name=user&email=user@domain.com', + W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('name=user&email=user@domain.com')) ) end diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 40c5ac2d09..e3ec5cf182 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -89,15 +89,21 @@ class MultipartParamsParsingTest < ActionController::IntegrationTest assert_equal 19512, file.size end + # Pending fix in Rack 1.2.2 + # http://rack.lighthouseapp.com/projects/22435-rack/tickets/79-multipart-handling-incorrectly-assuming-file-upload test "parses mixed files" do - params = parse_multipart('mixed_files') - assert_equal %w(files foo), params.keys.sort - assert_equal 'bar', params['foo'] - - # Rack doesn't handle multipart/mixed for us. - files = params['files'] - files.force_encoding('ASCII-8BIT') if files.respond_to?(:force_encoding) - assert_equal 19756, files.size + if Rack.release <= '1.2.1' + $stderr.puts 'multipart/mixed parsing pending fix in Rack 1.2.2' + else + params = parse_multipart('mixed_files') + assert_equal %w(files foo), params.keys.sort + assert_equal 'bar', params['foo'] + + # Rack doesn't handle multipart/mixed for us. + files = params['files'] + files.force_encoding('ASCII-8BIT') if files.respond_to?(:force_encoding) + assert_equal 19756, files.size + end end test "does not create tempfile if no file has been selected" do diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index 69dbd7f528..0bcef81534 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -141,6 +141,29 @@ class UrlEncodedParamsParsingTest < ActionController::IntegrationTest post "/parse", actual assert_response :ok assert_equal(expected, TestController.last_request_parameters) + assert_utf8(TestController.last_request_parameters) + end + end + + def assert_utf8(object) + return unless "ruby".encoding_aware? + + correct_encoding = Encoding.default_internal + + unless object.is_a?(Hash) + assert_equal correct_encoding, object.encoding, "#{object.inspect} should have been UTF-8" + return + end + + object.each do |k,v| + case v + when Hash + assert_utf8(v) + when Array + v.each {|el| assert_utf8(el) } + else + assert_utf8(v) + end end end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index e294703e72..26bd641cd6 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -16,6 +16,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest Routes = routes Routes.draw do default_url_options :host => "rubyonrails.org" + resources_path_names :correlation_indexes => "info_about_correlation_indexes" controller :sessions do get 'login' => :new @@ -34,6 +35,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + scope "bookmark", :controller => "bookmarks", :as => :bookmark do + get :new, :path => "build" + post :create, :path => "create", :as => "" + put :update + get "remove", :action => :destroy, :as => :remove + end + match 'account/logout' => redirect("/logout"), :as => :logout_redirect match 'account/login', :to => redirect("/login") @@ -68,12 +76,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get 'admin/accounts' => "queenbee#accounts" end - scope 'pt', :name_prefix => 'pt' do + get 'admin/passwords' => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor + + scope 'pt', :as => 'pt' do resources :projects, :path_names => { :edit => 'editar', :new => 'novo' }, :path => 'projetos' do post :preview, :on => :new + put :close, :on => :member, :path => 'fechar' + get :open, :on => :new, :path => 'abrir' end - resource :admin, :path_names => { :new => 'novo' }, :path => 'administrador' do + resource :admin, :path_names => { :new => 'novo', :activate => 'ativar' }, :path => 'administrador' do post :preview, :on => :new + put :activate, :on => :member end resources :products, :path_names => { :new => 'novo' } do new do @@ -84,6 +97,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest resources :projects, :controller => :project do resources :involvements, :attachments + get :correlation_indexes, :on => :collection resources :participants do put :update_all, :on => :collection @@ -110,6 +124,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end member do + get :some_path_with_name put :accessible_projects post :resend, :generate_new_password end @@ -142,6 +157,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest resources :comments, :except => :destroy end + resource :past, :only => :destroy + resource :present, :only => :update + resource :future, :only => :create + resources :relationships, :only => [:create, :destroy] + resources :friendships, :only => [:update] + shallow do namespace :api do resources :teams do @@ -174,6 +195,36 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + resources :customers do + get "recent" => "customers#recent", :as => :recent, :on => :collection + get "profile" => "customers#profile", :as => :profile, :on => :member + post "preview" => "customers#preview", :as => :preview, :on => :new + resource :avatar do + get "thumbnail(.:format)" => "avatars#thumbnail", :as => :thumbnail, :on => :member + end + resources :invoices do + get "outstanding" => "invoices#outstanding", :as => :outstanding, :on => :collection + get "overdue", :to => :overdue, :on => :collection + get "print" => "invoices#print", :as => :print, :on => :member + post "preview" => "invoices#preview", :as => :preview, :on => :new + end + resources :notes, :shallow => true do + get "preview" => "notes#preview", :as => :preview, :on => :new + get "print" => "notes#print", :as => :print, :on => :member + end + end + + namespace :api do + resources :customers do + get "recent" => "customers#recent", :as => :recent, :on => :collection + get "profile" => "customers#profile", :as => :profile, :on => :member + post "preview" => "customers#preview", :as => :preview, :on => :new + end + scope(':version', :version => /.+/) do + resources :users, :id => /.+?/, :format => /json|xml/ + end + end + match 'sprockets.js' => ::TestRoutingMapper::SprocketsApp match 'people/:id/update', :to => 'people#update', :as => :update_person @@ -207,10 +258,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + namespace :users, :path => 'usuarios' do + root :to => 'home#index' + end + controller :articles do - scope '/articles', :name_prefix => 'article' do + scope '/articles', :as => 'article' do scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do - match '/:id', :to => :with_id + match '/:id', :to => :with_id, :as => "" end end end @@ -243,8 +298,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest resource :dashboard, :constraints => { :ip => /192\.168\.1\.\d{1,3}/ } - scope :module => 'api' do + scope :module => :api do resource :token + resources :errors, :shallow => true do + resources :notices + end end scope :path => 'api' do @@ -252,6 +310,21 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest match '/' => 'mes#index' end + namespace :private do + root :to => redirect('/private/index') + match "index", :to => 'private#index' + end + + get "(/:username)/followers" => "followers#index" + get "/groups(/user/:username)" => "groups#index" + get "(/user/:username)/photos" => "photos#index" + + scope '(groups)' do + scope '(discussions)' do + resources :messages + end + end + match "whatever/:controller(/:action(/:id))" resource :profile do @@ -261,6 +334,21 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest post :preview end end + + resources :content + + scope :constraints => { :id => /\d+/ } do + get '/tickets', :to => 'tickets#index', :as => :tickets + end + + scope :constraints => { :id => /\d{4}/ } do + resources :movies do + resources :reviews + resource :trailer + end + end + + match '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ end end @@ -373,6 +461,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_namespace_redirect + with_test_routes do + get '/private' + assert_equal 301, @response.status + assert_equal 'http://www.example.com/private/index', @response.headers['Location'] + assert_equal 'Moved Permanently', @response.body + end + end + def test_session_singleton_resource with_test_routes do get '/session' @@ -455,6 +552,26 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_bookmarks + with_test_routes do + get '/bookmark/build' + assert_equal 'bookmarks#new', @response.body + assert_equal '/bookmark/build', new_bookmark_path + + post '/bookmark/create' + assert_equal 'bookmarks#create', @response.body + assert_equal '/bookmark/create', bookmark_path + + put '/bookmark' + assert_equal 'bookmarks#update', @response.body + assert_equal '/bookmark', update_bookmark_path + + get '/bookmark/remove' + assert_equal 'bookmarks#destroy', @response.body + assert_equal '/bookmark/remove', bookmark_remove_path + end + end + def test_admin with_test_routes do get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} @@ -468,6 +585,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} assert_equal 'pass', @response.headers['X-Cascade'] + + get '/admin/passwords', {}, {'REMOTE_ADDR' => '192.168.1.100'} + assert_equal 'queenbee#passwords', @response.body + + get '/admin/passwords', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] end end @@ -662,6 +785,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_projects_with_resources_path_names + with_test_routes do + get '/projects/info_about_correlation_indexes' + assert_equal 'project#correlation_indexes', @response.body + assert_equal '/projects/info_about_correlation_indexes', correlation_indexes_projects_path + end + end + def test_projects_posts with_test_routes do get '/projects/1/posts' @@ -729,6 +860,38 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_resource_routes_only_create_update_destroy + with_test_routes do + delete '/past' + assert_equal 'pasts#destroy', @response.body + assert_equal '/past', past_path + + put '/present' + assert_equal 'presents#update', @response.body + assert_equal '/present', present_path + + post '/future' + assert_equal 'futures#create', @response.body + assert_equal '/future', future_path + end + end + + def test_resources_routes_only_create_update_destroy + with_test_routes do + post '/relationships' + assert_equal 'relationships#create', @response.body + assert_equal '/relationships', relationships_path + + delete '/relationships/1' + assert_equal 'relationships#destroy', @response.body + assert_equal '/relationships/1', relationship_path(1) + + put '/friendships/1' + assert_equal 'friendships#update', @response.body + assert_equal '/friendships/1', friendship_path(1) + end + end + def test_resource_with_slugs_in_ids with_test_routes do get '/posts/rails-rocks' @@ -763,6 +926,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/pt/administrador/novo' assert_equal 'admins#new', @response.body assert_equal '/pt/administrador/novo', new_pt_admin_path + + put '/pt/administrador/ativar' + assert_equal 'admins#activate', @response.body + assert_equal '/pt/administrador/ativar', activate_pt_admin_path + end + end + + def test_path_option_override + with_test_routes do + get '/pt/projetos/novo/abrir' + assert_equal 'projects#open', @response.body + assert_equal '/pt/projetos/novo/abrir', open_new_pt_project_path + + put '/pt/projetos/1/fechar' + assert_equal 'projects#close', @response.body + assert_equal '/pt/projetos/1/fechar', close_pt_project_path(1) end end @@ -843,7 +1022,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/account/admin/subscription', account_admin_subscription_path end end - + def test_namespace_nested_in_resources with_test_routes do get '/clients/1/google/account' @@ -856,6 +1035,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_namespace_with_options + with_test_routes do + get '/usuarios' + assert_equal '/usuarios', users_root_path + assert_equal 'users/home#index', @response.body + end + end + def test_articles_with_id with_test_routes do get '/articles/rails/1' @@ -1257,6 +1444,190 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_custom_resource_routes_are_scoped + with_test_routes do + assert_equal '/customers/recent', recent_customers_path + assert_equal '/customers/1/profile', profile_customer_path(:id => '1') + assert_equal '/customers/new/preview', preview_new_customer_path + assert_equal '/customers/1/avatar/thumbnail.jpg', thumbnail_customer_avatar_path(:customer_id => '1', :format => :jpg) + assert_equal '/customers/1/invoices/outstanding', outstanding_customer_invoices_path(:customer_id => '1') + assert_equal '/customers/1/invoices/2/print', print_customer_invoice_path(:customer_id => '1', :id => '2') + assert_equal '/customers/1/invoices/new/preview', preview_new_customer_invoice_path(:customer_id => '1') + assert_equal '/customers/1/notes/new/preview', preview_new_customer_note_path(:customer_id => '1') + assert_equal '/notes/1/print', print_note_path(:id => '1') + assert_equal '/api/customers/recent', recent_api_customers_path + assert_equal '/api/customers/1/profile', profile_api_customer_path(:id => '1') + assert_equal '/api/customers/new/preview', preview_new_api_customer_path + + get '/customers/1/invoices/overdue' + assert_equal 'invoices#overdue', @response.body + end + end + + def test_shallow_nested_routes_ignore_module + with_test_routes do + get '/errors/1/notices' + assert_equal 'api/notices#index', @response.body + assert_equal '/errors/1/notices', error_notices_path(:error_id => '1') + + get '/notices/1' + assert_equal 'api/notices#show', @response.body + assert_equal '/notices/1', notice_path(:id => '1') + end + end + + def test_non_greedy_regexp + with_test_routes do + get '/api/1.0/users' + assert_equal 'api/users#index', @response.body + assert_equal '/api/1.0/users', api_users_path(:version => '1.0') + + get '/api/1.0/users.json' + assert_equal 'api/users#index', @response.body + assert_equal true, @request.format.json? + assert_equal '/api/1.0/users.json', api_users_path(:version => '1.0', :format => :json) + + get '/api/1.0/users/first.last' + assert_equal 'api/users#show', @response.body + assert_equal 'first.last', @request.params[:id] + assert_equal '/api/1.0/users/first.last', api_user_path(:version => '1.0', :id => 'first.last') + + get '/api/1.0/users/first.last.xml' + assert_equal 'api/users#show', @response.body + assert_equal 'first.last', @request.params[:id] + assert_equal true, @request.format.xml? + assert_equal '/api/1.0/users/first.last.xml', api_user_path(:version => '1.0', :id => 'first.last', :format => :xml) + end + end + + def test_glob_parameter_accepts_regexp + with_test_routes do + get '/en/path/to/existing/file.html' + assert_equal 200, @response.status + end + end + + def test_resources_controller_name_is_not_pluralized + with_test_routes do + get '/content' + assert_equal 'content#index', @response.body + end + end + + def test_url_generator_for_optional_prefix_dynamic_segment + with_test_routes do + get '/bob/followers' + assert_equal 'followers#index', @response.body + assert_equal 'http://www.example.com/bob/followers', + url_for(:controller => "followers", :action => "index", :username => "bob") + + get '/followers' + assert_equal 'followers#index', @response.body + assert_equal 'http://www.example.com/followers', + url_for(:controller => "followers", :action => "index", :username => nil) + end + end + + def test_url_generator_for_optional_suffix_static_and_dynamic_segment + with_test_routes do + get '/groups/user/bob' + assert_equal 'groups#index', @response.body + assert_equal 'http://www.example.com/groups/user/bob', + url_for(:controller => "groups", :action => "index", :username => "bob") + + get '/groups' + assert_equal 'groups#index', @response.body + assert_equal 'http://www.example.com/groups', + url_for(:controller => "groups", :action => "index", :username => nil) + end + end + + def test_url_generator_for_optional_prefix_static_and_dynamic_segment + with_test_routes do + get 'user/bob/photos' + assert_equal 'photos#index', @response.body + assert_equal 'http://www.example.com/user/bob/photos', + url_for(:controller => "photos", :action => "index", :username => "bob") + + get 'photos' + assert_equal 'photos#index', @response.body + assert_equal 'http://www.example.com/photos', + url_for(:controller => "photos", :action => "index", :username => nil) + end + end + + def test_url_recognition_for_optional_static_segments + with_test_routes do + get '/groups/discussions/messages' + assert_equal 'messages#index', @response.body + + get '/groups/discussions/messages/1' + assert_equal 'messages#show', @response.body + + get '/groups/messages' + assert_equal 'messages#index', @response.body + + get '/groups/messages/1' + assert_equal 'messages#show', @response.body + + get '/discussions/messages' + assert_equal 'messages#index', @response.body + + get '/discussions/messages/1' + assert_equal 'messages#show', @response.body + + get '/messages' + assert_equal 'messages#index', @response.body + + get '/messages/1' + assert_equal 'messages#show', @response.body + end + end + + def test_router_removes_invalid_conditions + with_test_routes do + get '/tickets' + assert_equal 'tickets#index', @response.body + assert_equal '/tickets', tickets_path + end + end + + def test_constraints_are_merged_from_scope + with_test_routes do + get '/movies/0001' + assert_equal 'movies#show', @response.body + assert_equal '/movies/0001', movie_path(:id => '0001') + + get '/movies/00001' + assert_equal 'Not Found', @response.body + assert_raises(ActionController::RoutingError){ movie_path(:id => '00001') } + + get '/movies/0001/reviews' + assert_equal 'reviews#index', @response.body + assert_equal '/movies/0001/reviews', movie_reviews_path(:movie_id => '0001') + + get '/movies/00001/reviews' + assert_equal 'Not Found', @response.body + assert_raises(ActionController::RoutingError){ movie_reviews_path(:movie_id => '00001') } + + get '/movies/0001/reviews/0001' + assert_equal 'reviews#show', @response.body + assert_equal '/movies/0001/reviews/0001', movie_review_path(:movie_id => '0001', :id => '0001') + + get '/movies/00001/reviews/0001' + assert_equal 'Not Found', @response.body + assert_raises(ActionController::RoutingError){ movie_path(:movie_id => '00001', :id => '00001') } + + get '/movies/0001/trailer' + assert_equal 'trailers#show', @response.body + assert_equal '/movies/0001/trailer', movie_trailer_path(:movie_id => '0001') + + get '/movies/00001/trailer' + assert_equal 'Not Found', @response.body + assert_raises(ActionController::RoutingError){ movie_trailer_path(:movie_id => '00001') } + end + end + private def with_test_routes yield diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index b4380f7818..fd63f5ad5e 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -83,7 +83,7 @@ class CookieStoreTest < ActionController::IntegrationTest get '/get_session_id' assert_response :success - assert_equal "id: #{session_id}", response.body + assert_equal "id: #{session_id}", response.body, "should be able to read session id without accessing the session hash" end end @@ -96,6 +96,31 @@ class CookieStoreTest < ActionController::IntegrationTest end end + # {:foo=>#<SessionAutoloadTest::Foo bar:"baz">, :session_id=>"ce8b0752a6ab7c7af3cdb8a80e6b9e46"} + SignedSerializedCookie = "BAh7BzoIZm9vbzodU2Vzc2lvbkF1dG9sb2FkVGVzdDo6Rm9vBjoJQGJhciIIYmF6Og9zZXNzaW9uX2lkIiVjZThiMDc1MmE2YWI3YzdhZjNjZGI4YTgwZTZiOWU0Ng==--2bf3af1ae8bd4e52b9ac2099258ace0c380e601c" + + def test_deserializes_unloaded_classes_on_get_id + with_test_route_set do + with_autoload_path "session_autoload_test" do + cookies[SessionKey] = SignedSerializedCookie + get '/get_session_id' + assert_response :success + assert_equal 'id: ce8b0752a6ab7c7af3cdb8a80e6b9e46', response.body, "should auto-load unloaded class" + end + end + end + + def test_deserializes_unloaded_classes_on_get_value + with_test_route_set do + with_autoload_path "session_autoload_test" do + cookies[SessionKey] = SignedSerializedCookie + get '/get_session_value' + assert_response :success + assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class" + end + end + end + def test_close_raises_when_data_overflows with_test_route_set do assert_raise(ActionDispatch::Cookies::CookieOverflow) { @@ -141,6 +166,15 @@ class CookieStoreTest < ActionController::IntegrationTest end end + def test_getting_from_nonexistent_session + with_test_route_set do + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body + assert_nil headers['Set-Cookie'], "should only create session on write, not read" + end + end + def test_persistent_session_id with_test_route_set do cookies[SessionKey] = SignedBar @@ -196,21 +230,21 @@ class CookieStoreTest < ActionController::IntegrationTest def test_session_store_without_domain with_test_route_set do get '/set_session_value' - assert_no_match /domain\=/, headers['Set-Cookie'] + assert_no_match(/domain\=/, headers['Set-Cookie']) end end def test_session_store_with_nil_domain with_test_route_set(:domain => nil) do get '/set_session_value' - assert_no_match /domain\=/, headers['Set-Cookie'] + assert_no_match(/domain\=/, headers['Set-Cookie']) end end def test_session_store_with_all_domains with_test_route_set(:domain => :all) do get '/set_session_value' - assert_match /domain=\.example\.com/, headers['Set-Cookie'] + assert_match(/domain=\.example\.com/, headers['Set-Cookie']) end end @@ -238,4 +272,5 @@ class CookieStoreTest < ActionController::IntegrationTest yield end end + end diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index 8858a398e0..9bd6f9b8c4 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -11,13 +11,17 @@ class MemCacheStoreTest < ActionController::IntegrationTest session[:foo] = "bar" head :ok end + + def set_serialized_session_value + session[:foo] = SessionAutoloadTest::Foo.new + head :ok + end def get_session_value render :text => "foo: #{session[:foo].inspect}" end def get_session_id - session[:foo] render :text => "#{request.session_options[:id]}" end @@ -56,6 +60,34 @@ class MemCacheStoreTest < ActionController::IntegrationTest end end + def test_getting_session_value_after_session_reset + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + session_cookie = cookies.send(:hash_for)['_session_id'] + + get '/call_reset_session' + assert_response :success + assert_not_equal [], headers['Set-Cookie'] + + cookies << session_cookie # replace our new session_id with our old, pre-reset session_id + + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from memcached" + end + end + + def test_getting_from_nonexistent_session + with_test_route_set do + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body + assert_nil cookies['_session_id'], "should only create session on write, not read" + end + end + def test_setting_session_value_after_session_reset with_test_route_set do get '/set_session_value' @@ -86,7 +118,38 @@ class MemCacheStoreTest < ActionController::IntegrationTest get '/get_session_id' assert_response :success - assert_equal session_id, response.body + assert_equal session_id, response.body, "should be able to read session id without accessing the session hash" + end + end + + def test_deserializes_unloaded_class + with_test_route_set do + with_autoload_path "session_autoload_test" do + get '/set_serialized_session_value' + assert_response :success + assert cookies['_session_id'] + end + with_autoload_path "session_autoload_test" do + get '/get_session_id' + assert_response :success + end + with_autoload_path "session_autoload_test" do + get '/get_session_value' + assert_response :success + assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class" + end + end + end + + def test_doesnt_write_session_cookie_if_session_id_is_already_exists + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + + get '/get_session_value' + assert_response :success + assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists" end end diff --git a/actionpack/test/fixtures/fun/games/_form.erb b/actionpack/test/fixtures/fun/games/_form.erb new file mode 100644 index 0000000000..01107f1cb2 --- /dev/null +++ b/actionpack/test/fixtures/fun/games/_form.erb @@ -0,0 +1 @@ +<%= form.label :title %> diff --git a/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb b/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb new file mode 100644 index 0000000000..4ee7a24561 --- /dev/null +++ b/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb @@ -0,0 +1,10 @@ +module SessionAutoloadTest + class Foo + def initialize(bar='baz') + @bar = bar + end + def inspect + "#<#{self.class} bar:#{@bar.inspect}>" + end + end +end diff --git a/actionpack/test/template/active_model_helper_test.rb b/actionpack/test/template/active_model_helper_test.rb index b1705072c2..6ab244d178 100644 --- a/actionpack/test/template/active_model_helper_test.rb +++ b/actionpack/test/template/active_model_helper_test.rb @@ -39,6 +39,13 @@ class ActiveModelHelperTest < ActionView::TestCase ) end + def test_hidden_field_does_not_render_errors + assert_dom_equal( + %(<input id="post_author_name" name="post[author_name]" type="hidden" value="" />), + hidden_field("post", "author_name") + ) + end + def test_field_error_proc old_proc = ActionView::Base.field_error_proc ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 633641514e..6d5e4893c4 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -44,7 +44,7 @@ class AssetTagHelperTest < ActionView::TestCase @controller.request = @request - ActionView::Helpers::AssetTagHelper::reset_javascript_include_default + ActionView::Helpers::AssetTagHelper::register_javascript_expansion :defaults => ['prototype', 'effects', 'dragdrop', 'controls', 'rails'] end def url_for(*args) @@ -256,19 +256,6 @@ class AssetTagHelperTest < ActionView::TestCase assert javascript_include_tag("prototype").html_safe? end - def test_register_javascript_include_default - ENV["RAILS_ASSET_ID"] = "" - ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'bank' - assert_dom_equal %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag(:defaults) - end - - def test_register_javascript_include_default_mixed_defaults - ENV["RAILS_ASSET_ID"] = "" - ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'bank' - ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'robber', '/elsewhere/cools.js' - assert_dom_equal %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/elsewhere/cools.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag(:defaults) - end - def test_custom_javascript_expansions ENV["RAILS_ASSET_ID"] = "" ActionView::Helpers::AssetTagHelper::register_javascript_expansion :robbery => ["bank", "robber"] @@ -286,6 +273,11 @@ class AssetTagHelperTest < ActionView::TestCase assert_raise(ArgumentError) { javascript_include_tag('first', :monkey, 'last') } end + def test_reset_javascript_expansions + ActionView::Helpers::AssetTagHelper.javascript_expansions.clear + assert_raise(ArgumentError) { javascript_include_tag(:defaults) } + end + def test_stylesheet_path ENV["RAILS_ASSET_ID"] = "" StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } @@ -923,7 +915,7 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase @request = Struct.new(:protocol).new("gopher://") @controller.request = @request - ActionView::Helpers::AssetTagHelper::reset_javascript_include_default + ActionView::Helpers::AssetTagHelper.javascript_expansions.clear end def url_for(options) diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index 053fcc4d24..a1db49d4d0 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -882,6 +882,33 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) end + def test_select_date_with_separator_and_discard_day + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << " / " + + expected << %(<select id="date_first_month" name="date[first][month]">\n) + expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) + expected << "</select>\n" + + expected << %(<input type="hidden" id="date_first_day" name="date[first][day]" value="1" />\n) + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) + end + + def test_select_date_with_separator_discard_month_and_day + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + expected << %(<input type="hidden" id="date_first_month" name="date[first][month]" value="8" />\n) + expected << %(<input type="hidden" id="date_first_day" name="date[first][day]" value="16" />\n) + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_month => true, :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) + end + def test_select_datetime expected = %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) diff --git a/actionpack/test/template/erb/form_for_test.rb b/actionpack/test/template/erb/form_for_test.rb index ec6e872735..e722b40a9a 100644 --- a/actionpack/test/template/erb/form_for_test.rb +++ b/actionpack/test/template/erb/form_for_test.rb @@ -5,7 +5,7 @@ module ERBTest class TagHelperTest < BlockTestCase test "form_for works" do output = render_content "form_for(:staticpage, :url => {:controller => 'blah', :action => 'update'})", "" - assert_equal "<form action=\"/blah/update\" method=\"post\"></form>", output + assert_match %r{<form.*action="/blah/update".*method="post">.*</form>}, output end end end diff --git a/actionpack/test/template/erb/tag_helper_test.rb b/actionpack/test/template/erb/tag_helper_test.rb index 64a88bde1d..d073100986 100644 --- a/actionpack/test/template/erb/tag_helper_test.rb +++ b/actionpack/test/template/erb/tag_helper_test.rb @@ -28,8 +28,8 @@ module ERBTest end test "percent equals works with form tags" do - expected_output = "<form action=\"foo\" method=\"post\">hello</form>" - maybe_deprecated { assert_equal expected_output, render_content("form_tag('foo')", "<%= 'hello' %>") } + expected_output = %r{<form.*action="foo".*method="post">.*hello*</form>} + maybe_deprecated { assert_match expected_output, render_content("form_tag('foo')", "<%= 'hello' %>") } end test "percent equals works with fieldset tags" do diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 2f3869994c..4b9e41803f 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -188,6 +188,11 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, text_field("post", "title", :maxlength => 35, :size => nil) end + def test_text_field_with_nil_value + expected = '<input id="post_title" name="post[title]" size="30" type="text" />' + assert_dom_equal expected, text_field("post", "title", :value => nil) + end + def test_text_field_doesnt_change_param_values object_name = 'post[]' expected = '<input id="post_123_title" name="post[123][title]" size="30" type="text" value="Hello World" />' @@ -208,6 +213,11 @@ class FormHelperTest < ActionView::TestCase hidden_field("post", "title") end + def test_hidden_field_with_nil_value + expected = '<input id="post_title" name="post[title]" type="hidden" />' + assert_dom_equal expected, hidden_field("post", "title", :value => nil) + end + def test_text_field_with_options assert_dom_equal '<input id="post_title" name="post[title]" type="hidden" value="Something Else" />', hidden_field("post", "title", :value => "Something Else") @@ -300,6 +310,11 @@ class FormHelperTest < ActionView::TestCase ) end + def test_radio_button_with_negative_integer_value + assert_dom_equal('<input id="post_secret_-1" name="post[secret]" type="radio" value="-1" />', + radio_button("post", "secret", "-1")) + end + def test_radio_button_respects_passed_in_id assert_dom_equal('<input checked="checked" id="foo" name="post[secret]" type="radio" value="1" />', radio_button("post", "secret", "1", :id=>"foo") @@ -568,7 +583,8 @@ class FormHelperTest < ActionView::TestCase end expected = - "<form action='http://www.example.com' id='create-post' method='post'>" + + "<form accept-charset='UTF-8' action='http://www.example.com' id='create-post' method='post'>" + + snowman + "<label for='post_title'>The Title</label>" + "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + @@ -589,15 +605,14 @@ class FormHelperTest < ActionView::TestCase concat f.submit('Create post') end - expected = - "<form class='other_name_edit' method='post' action='/posts/123' id='create-post'>" + - "<div style='margin:0;padding:0;display:inline'><input name='_method' value='put' type='hidden' /></div>" + + expected = whole_form("/posts/123", "create-post", "other_name_edit", :method => "put") do "<label for='other_name_title'>Title</label>" + "<input name='other_name[title]' size='30' id='other_name_title' value='Hello World' type='text' />" + "<textarea name='other_name[body]' id='other_name_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='other_name[secret]' value='0' type='hidden' />" + "<input name='other_name[secret]' checked='checked' id='other_name_secret' value='1' type='checkbox' />" + - "<input name='commit' id='other_name_submit' value='Create post' type='submit' /></form>" + "<input name='commit' id='other_name_submit' value='Create post' type='submit' />" + end assert_dom_equal expected, output_buffer end @@ -611,14 +626,12 @@ class FormHelperTest < ActionView::TestCase end end - expected = - "<form action='http://www.example.com' id='create-post' method='post'>" + - "<div style='margin:0;padding:0;display:inline'><input name='_method' type='hidden' value='put' /></div>" + + expected = whole_form("http://www.example.com", "create-post", nil, "put") do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + - "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + - "</form>" + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + end assert_dom_equal expected, output_buffer end @@ -632,14 +645,31 @@ class FormHelperTest < ActionView::TestCase end end - expected = - "<form action='http://www.example.com' id='create-post' method='post' data-remote='true'>" + - "<div style='margin:0;padding:0;display:inline'><input name='_method' type='hidden' value='put' /></div>" + + expected = whole_form("http://www.example.com", "create-post", nil, :method => "put", :remote => true) do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + - "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + - "</form>" + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_remote_without_html + assert_deprecated do + form_for(:post, @post, :remote => true) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + end + + expected = whole_form("http://www.example.com", nil, nil, :remote => true) do + "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + + "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + end assert_dom_equal expected, output_buffer end @@ -651,13 +681,12 @@ class FormHelperTest < ActionView::TestCase concat f.check_box(:secret) end - expected = - "<form action='http://www.example.com' id='create-post' method='post'>" + + expected = whole_form("http://www.example.com", "create-post") do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + - "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + - "</form>" + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + end assert_dom_equal expected, output_buffer end @@ -672,14 +701,13 @@ class FormHelperTest < ActionView::TestCase end end - expected = - "<form action='http://www.example.com' method='post'>" + + expected = whole_form do "<label for='post_123_title'>Title</label>" + "<input name='post[123][title]' size='30' type='text' id='post_123_title' value='Hello World' />" + "<textarea name='post[123][body]' id='post_123_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[123][secret]' type='hidden' value='0' />" + - "<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />" + - "</form>" + "<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />" + end assert_dom_equal expected, output_buffer end @@ -693,13 +721,12 @@ class FormHelperTest < ActionView::TestCase end end - expected = - "<form action='http://www.example.com' method='post'>" + + expected = whole_form do "<input name='post[][title]' size='30' type='text' id='post__title' value='Hello World' />" + "<textarea name='post[][body]' id='post__body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[][secret]' type='hidden' value='0' />" + - "<input name='post[][secret]' checked='checked' type='checkbox' id='post__secret' value='1' />" + - "</form>" + "<input name='post[][secret]' checked='checked' type='checkbox' id='post__secret' value='1' />" + end assert_dom_equal expected, output_buffer end @@ -714,9 +741,10 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='commit' id='post_submit' type='submit' value='Create Post' />" + - "</form>" + expected = whole_form do + "<input name='commit' id='post_submit' type='submit' value='Create Post' />" + end + assert_dom_equal expected, output_buffer ensure I18n.locale = old_locale @@ -731,9 +759,10 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='commit' id='post_submit' type='submit' value='Confirm Post changes' />" + - "</form>" + expected = whole_form do + "<input name='commit' id='post_submit' type='submit' value='Confirm Post changes' />" + end + assert_dom_equal expected, output_buffer ensure I18n.locale = old_locale @@ -746,9 +775,10 @@ class FormHelperTest < ActionView::TestCase concat f.submit :class => "extra" end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='commit' class='extra' id='post_submit' type='submit' value='Save changes' />" + - "</form>" + expected = whole_form do + "<input name='commit' class='extra' id='post_submit' type='submit' value='Save changes' />" + end + assert_dom_equal expected, output_buffer ensure I18n.locale = old_locale @@ -763,9 +793,10 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='commit' id='another_post_submit' type='submit' value='Update your Post' />" + - "</form>" + expected = whole_form do + "<input name='commit' id='another_post_submit' type='submit' value='Update your Post' />" + end + assert_dom_equal expected, output_buffer ensure I18n.locale = old_locale @@ -780,9 +811,9 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='post[comment][title]' size='30' type='text' id='post_comment_title' value='Hello World' />" + - "</form>" + expected = whole_form do + "<input name='post[comment][title]' size='30' type='text' id='post_comment_title' value='Hello World' />" + end assert_dom_equal expected, output_buffer end @@ -797,10 +828,10 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='post[123][title]' size='30' type='text' id='post_123_title' value='Hello World' />" + - "<input name='post[123][comment][][name]' size='30' type='text' id='post_123_comment__name' value='new comment' />" + - "</form>" + expected = whole_form do + "<input name='post[123][title]' size='30' type='text' id='post_123_title' value='Hello World' />" + + "<input name='post[123][comment][][name]' size='30' type='text' id='post_123_comment__name' value='new comment' />" + end assert_dom_equal expected, output_buffer end @@ -815,10 +846,10 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='post[1][title]' size='30' type='text' id='post_1_title' value='Hello World' />" + - "<input name='post[1][comment][1][name]' size='30' type='text' id='post_1_comment_1_name' value='new comment' />" + - "</form>" + expected = whole_form do + "<input name='post[1][title]' size='30' type='text' id='post_1_title' value='Hello World' />" + + "<input name='post[1][comment][1][name]' size='30' type='text' id='post_1_comment_1_name' value='new comment' />" + end assert_dom_equal expected, output_buffer end @@ -832,9 +863,9 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='post[1][comment][title]' size='30' type='text' id='post_1_comment_title' value='Hello World' />" + - "</form>" + expected = whole_form do + "<input name='post[1][comment][title]' size='30' type='text' id='post_1_comment_title' value='Hello World' />" + end assert_dom_equal expected, output_buffer end @@ -848,9 +879,9 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='post[1][comment][5][title]' size='30' type='text' id='post_1_comment_5_title' value='Hello World' />" + - "</form>" + expected = whole_form do + "<input name='post[1][comment][5][title]' size='30' type='text' id='post_1_comment_5_title' value='Hello World' />" + end assert_dom_equal expected, output_buffer end @@ -864,9 +895,9 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='post[123][comment][title]' size='30' type='text' id='post_123_comment_title' value='Hello World' />" + - "</form>" + expected = whole_form do + "<input name='post[123][comment][title]' size='30' type='text' id='post_123_comment_title' value='Hello World' />" + end assert_dom_equal expected, output_buffer end @@ -880,9 +911,9 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='post[comment][5][title]' type='radio' id='post_comment_5_title_hello' value='hello' />" + - "</form>" + expected = whole_form do + "<input name='post[comment][5][title]' type='radio' id='post_comment_5_title_hello' value='hello' />" + end assert_dom_equal expected, output_buffer end @@ -896,9 +927,9 @@ class FormHelperTest < ActionView::TestCase end end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='post[123][comment][123][title]' size='30' type='text' id='post_123_comment_123_title' value='Hello World' />" + - "</form>" + expected = whole_form do + "<input name='post[123][comment][123][title]' size='30' type='text' id='post_123_comment_123_title' value='Hello World' />" + end assert_dom_equal expected, output_buffer end @@ -917,12 +948,11 @@ class FormHelperTest < ActionView::TestCase } end - expected = "<form action='http://www.example.com' method='post'>" + - "<input name='post[123][comment][5][title]' size='30' type='text' id='post_123_comment_5_title' value='Hello World' />" + - "</form>" + - "<form action='http://www.example.com' method='post'>" + - "<input name='post[1][comment][123][title]' size='30' type='text' id='post_1_comment_123_title' value='Hello World' />" + - "</form>" + expected = whole_form do + "<input name='post[123][comment][5][title]' size='30' type='text' id='post_123_comment_5_title' value='Hello World' />" + end + whole_form do + "<input name='post[1][comment][123][title]' size='30' type='text' id='post_1_comment_123_title' value='Hello World' />" + end assert_dom_equal expected, output_buffer end @@ -940,10 +970,10 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="new author" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="new author" />' + end assert_dom_equal expected, output_buffer end @@ -971,11 +1001,11 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + end assert_dom_equal expected, output_buffer end @@ -993,11 +1023,11 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + end assert_dom_equal expected, output_buffer end @@ -1016,13 +1046,13 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end assert_dom_equal expected, output_buffer end @@ -1042,13 +1072,13 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + end assert_dom_equal expected, output_buffer end @@ -1067,11 +1097,11 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="new comment" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="new comment" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' + end assert_dom_equal expected, output_buffer end @@ -1090,12 +1120,12 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' + end assert_dom_equal expected, output_buffer end @@ -1110,9 +1140,9 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + end assert_dom_equal expected, output_buffer end @@ -1129,13 +1159,13 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end assert_dom_equal expected, output_buffer end @@ -1153,13 +1183,13 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end assert_dom_equal expected, output_buffer end @@ -1178,12 +1208,12 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' + - '</form>' + expected = whole_form do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' + end assert_dom_equal expected, output_buffer assert_equal yielded_comments, @post.comments @@ -1200,10 +1230,10 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" size="30" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' + - '</form>' + expected = whole_form do + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" size="30" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' + end assert_dom_equal expected, output_buffer end @@ -1238,20 +1268,20 @@ class FormHelperTest < ActionView::TestCase end end - expected = '<form action="http://www.example.com" method="post">' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" size="30" type="text" value="commentrelevance #314" />' + - '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + - '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" size="30" type="text" value="tag #123" />' + - '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" size="30" type="text" value="tagrelevance #3141" />' + - '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' + - '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' + - '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" size="30" type="text" value="tag #456" />' + - '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" size="30" type="text" value="tagrelevance #31415" />' + - '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' + - '<input id="post_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />' + - '</form>' + expected = whole_form do + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" size="30" type="text" value="commentrelevance #314" />' + + '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" size="30" type="text" value="tag #123" />' + + '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" size="30" type="text" value="tagrelevance #3141" />' + + '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' + + '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' + + '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" size="30" type="text" value="tag #456" />' + + '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" size="30" type="text" value="tagrelevance #31415" />' + + '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' + + '<input id="post_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />' + end assert_dom_equal expected, output_buffer end @@ -1391,7 +1421,8 @@ class FormHelperTest < ActionView::TestCase end expected = - "<form action='http://www.example.com' id='create-post' method='post'>" + + "<form accept-charset='UTF-8' action='http://www.example.com' id='create-post' method='post'>" + + snowman + "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='parent_post[secret]' type='hidden' value='0' />" + @@ -1414,11 +1445,11 @@ class FormHelperTest < ActionView::TestCase end expected = - "<form action='http://www.example.com' id='create-post' method='post'>" + - "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + - "<input name='post[comment][name]' type='text' id='post_comment_name' value='new comment' size='30' />" + - "</form>" + whole_form("http://www.example.com", "create-post") do + "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + + "<input name='post[comment][name]' type='text' id='post_comment_name' value='new comment' size='30' />" + end assert_dom_equal expected, output_buffer end @@ -1442,16 +1473,42 @@ class FormHelperTest < ActionView::TestCase end end - expected = - "<form action='http://www.example.com' method='post'>" + - "<label for='title'>Title:</label> <input name='post[title]' size='30' type='text' id='post_title' value='Hello World' /><br/>" + - "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea><br/>" + - "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" + - "</form>" + expected = whole_form do + "<label for='title'>Title:</label> <input name='post[title]' size='30' type='text' id='post_title' value='Hello World' /><br/>" + + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea><br/>" + + "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" + end assert_dom_equal expected, output_buffer end + def snowman(method = nil) + txt = %{<div style="margin:0;padding:0;display:inline">} + txt << %{<input name="_snowman" type="hidden" value="☃" />} + txt << %{<input name="_method" type="hidden" value="#{method}" />} if method + txt << %{</div>} + end + + def form_text(action = "http://www.example.com", id = nil, html_class = nil, remote = nil) + txt = %{<form accept-charset="UTF-8" action="#{action}"} + txt << %{ data-remote="true"} if remote + txt << %{ class="#{html_class}"} if html_class + txt << %{ id="#{id}"} if id + txt << %{ method="post">} + end + + def whole_form(action = "http://www.example.com", id = nil, html_class = nil, options = nil) + contents = block_given? ? yield : "" + + if options.is_a?(Hash) + method, remote = options.values_at(:method, :remote) + else + method = options + end + + form_text(action, id, html_class, remote) + snowman(method) + contents + "</form>" + end + def test_default_form_builder old_default_form_builder, ActionView::Base.default_form_builder = ActionView::Base.default_form_builder, LabelledFormBuilder @@ -1464,12 +1521,11 @@ class FormHelperTest < ActionView::TestCase end end - expected = - "<form action='http://www.example.com' method='post'>" + + expected = whole_form do "<label for='title'>Title:</label> <input name='post[title]' size='30' type='text' id='post_title' value='Hello World' /><br/>" + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea><br/>" + - "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" + - "</form>" + "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" + end assert_dom_equal expected, output_buffer ensure @@ -1542,7 +1598,7 @@ class FormHelperTest < ActionView::TestCase assert_deprecated do form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end end - expected = "<form action=\"http://www.example.com\" class=\"some_class\" id=\"some_form\" method=\"post\"></form>" + expected = whole_form("http://www.example.com", "some_form", "some_class") assert_dom_equal expected, output_buffer end @@ -1552,7 +1608,8 @@ class FormHelperTest < ActionView::TestCase form_for(:post, @post, :url => 'http://www.otherdomain.com') do |f| end end - assert_equal '<form action="http://www.otherdomain.com" method="post"></form>', output_buffer + assert_equal whole_form("http://www.otherdomain.com"), output_buffer + # assert_equal '<form action="http://www.otherdomain.com" method="post"></form>', output_buffer end def test_form_for_with_hash_url_option @@ -1569,14 +1626,15 @@ class FormHelperTest < ActionView::TestCase form_for(:post, @post, :url => @post) do |f| end end - expected = "<form action=\"/posts/123\" method=\"post\"></form>" + expected = whole_form("/posts/123") + # expected = "<form action=\"/posts/123\" method=\"post\"></form>" assert_equal expected, output_buffer end def test_form_for_with_existing_object form_for(@post) do |f| end - expected = "<form action=\"/posts/123\" class=\"edit_post\" id=\"edit_post_123\" method=\"post\"><div style=\"margin:0;padding:0;display:inline\"><input name=\"_method\" type=\"hidden\" value=\"put\" /></div></form>" + expected = whole_form("/posts/123", "edit_post_123", "edit_post", "put") assert_equal expected, output_buffer end @@ -1587,7 +1645,7 @@ class FormHelperTest < ActionView::TestCase form_for(post) do |f| end - expected = "<form action=\"/posts\" class=\"new_post\" id=\"new_post\" method=\"post\"></form>" + expected = whole_form("/posts", "new_post", "new_post") assert_equal expected, output_buffer end @@ -1595,14 +1653,14 @@ class FormHelperTest < ActionView::TestCase @comment.save form_for([@post, @comment]) {} - expected = %(<form action="#{comment_path(@post, @comment)}" class="edit_comment" id="edit_comment_1" method="post"><div style="margin:0;padding:0;display:inline"><input name="_method" type="hidden" value="put" /></div></form>) + expected = whole_form(comment_path(@post, @comment), "edit_comment_1", "edit_comment", "put") assert_dom_equal expected, output_buffer end def test_form_for_with_new_object_in_list form_for([@post, @comment]) {} - expected = %(<form action="#{comments_path(@post)}" class="new_comment" id="new_comment" method="post"></form>) + expected = whole_form(comments_path(@post), "new_comment", "new_comment") assert_dom_equal expected, output_buffer end @@ -1610,21 +1668,21 @@ class FormHelperTest < ActionView::TestCase @comment.save form_for([:admin, @post, @comment]) {} - expected = %(<form action="#{admin_comment_path(@post, @comment)}" class="edit_comment" id="edit_comment_1" method="post"><div style="margin:0;padding:0;display:inline"><input name="_method" type="hidden" value="put" /></div></form>) + expected = whole_form(admin_comment_path(@post, @comment), "edit_comment_1", "edit_comment", "put") assert_dom_equal expected, output_buffer end def test_form_for_with_new_object_and_namespace_in_list form_for([:admin, @post, @comment]) {} - expected = %(<form action="#{admin_comments_path(@post)}" class="new_comment" id="new_comment" method="post"></form>) + expected = whole_form(admin_comments_path(@post), "new_comment", "new_comment") assert_dom_equal expected, output_buffer end def test_form_for_with_existing_object_and_custom_url form_for(@post, :url => "/super_posts") do |f| end - expected = "<form action=\"/super_posts\" class=\"edit_post\" id=\"edit_post_123\" method=\"post\"><div style=\"margin:0;padding:0;display:inline\"><input name=\"_method\" type=\"hidden\" value=\"put\" /></div></form>" + expected = whole_form("/super_posts", "edit_post_123", "edit_post", "put") assert_equal expected, output_buffer end diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index 19b73aa810..65b5f5ccc1 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -177,17 +177,16 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_option_groups_from_collection_for_select - @continents = [ - Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")] ), - Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] ) - ] - assert_dom_equal( "<optgroup label=\"<Africa>\"><option value=\"<sa>\"><South Africa></option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>", - option_groups_from_collection_for_select(@continents, "countries", "continent_name", "country_id", "country_name", "dk") + option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk") ) end + def test_option_groups_from_collection_for_select_returns_html_safe_string + assert option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk").html_safe? + end + def test_grouped_options_for_select_with_array assert_dom_equal( "<optgroup label=\"North America\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>", @@ -824,31 +823,21 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_grouped_collection_select - @continents = [ - Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")] ), - Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] ) - ] - @post = Post.new @post.origin = 'dk' assert_dom_equal( %Q{<select id="post_origin" name="post[origin]"><optgroup label="<Africa>"><option value="<sa>"><South Africa></option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>}, - grouped_collection_select("post", "origin", @continents, :countries, :continent_name, :country_id, :country_name) + grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name) ) end def test_grouped_collection_select_under_fields_for - @continents = [ - Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")] ), - Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] ) - ] - @post = Post.new @post.origin = 'dk' output_buffer = fields_for :post, @post do |f| - concat f.grouped_collection_select("origin", @continents, :countries, :continent_name, :country_id, :country_name) + concat f.grouped_collection_select("origin", dummy_continents, :countries, :continent_name, :country_id, :country_name) end assert_dom_equal( @@ -864,4 +853,9 @@ class FormOptionsHelperTest < ActionView::TestCase Post.new("Babe went home", "Babe", "To a little house", "shh!"), Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ] end + + def dummy_continents + [ Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")] ), + Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] ) ] + end end diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index b75863bb6a..8a0f352bc0 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -8,6 +8,36 @@ class FormTagHelperTest < ActionView::TestCase @controller = BasicController.new end + def snowman(options = {}) + method = options[:method] + + txt = %{<div style="margin:0;padding:0;display:inline">} + txt << %{<input name="_snowman" type="hidden" value="☃" />} + txt << %{<input name="_method" type="hidden" value="#{method}" />} if method + txt << %{</div>} + end + + def form_text(action = "http://www.example.com", options = {}) + remote, enctype, html_class, id = options.values_at(:remote, :enctype, :html_class, :id) + + txt = %{<form accept-charset="UTF-8" action="#{action}"} + txt << %{ enctype="multipart/form-data"} if enctype + txt << %{ data-remote="true"} if remote + txt << %{ class="#{html_class}"} if html_class + txt << %{ id="#{id}"} if id + txt << %{ method="post">} + end + + def whole_form(action = "http://www.example.com", options = {}) + out = form_text(action, options) + snowman(options) + + if block_given? + out << yield << "</form>" + end + + out + end + def url_for(options) if options.is_a?(Hash) "http://www.example.com" @@ -31,51 +61,57 @@ class FormTagHelperTest < ActionView::TestCase def test_form_tag actual = form_tag - expected = %(<form action="http://www.example.com" method="post">) + expected = whole_form assert_dom_equal expected, actual end def test_form_tag_multipart actual = form_tag({}, { 'multipart' => true }) - expected = %(<form action="http://www.example.com" enctype="multipart/form-data" method="post">) + expected = whole_form("http://www.example.com", :enctype => true) assert_dom_equal expected, actual end def test_form_tag_with_method_put actual = form_tag({}, { :method => :put }) - expected = %(<form action="http://www.example.com" method="post"><div style='margin:0;padding:0;display:inline'><input type="hidden" name="_method" value="put" /></div>) + expected = whole_form("http://www.example.com", :method => :put) assert_dom_equal expected, actual end def test_form_tag_with_method_delete actual = form_tag({}, { :method => :delete }) - expected = %(<form action="http://www.example.com" method="post"><div style='margin:0;padding:0;display:inline'><input type="hidden" name="_method" value="delete" /></div>) + + expected = whole_form("http://www.example.com", :method => :delete) assert_dom_equal expected, actual end def test_form_tag_with_remote actual = form_tag({}, :remote => true) - expected = %(<form action="http://www.example.com" method="post" data-remote="true">) + + expected = whole_form("http://www.example.com", :remote => true) assert_dom_equal expected, actual end def test_form_tag_with_remote_false actual = form_tag({}, :remote => false) - expected = %(<form action="http://www.example.com" method="post">) + + expected = whole_form assert_dom_equal expected, actual end def test_form_tag_with_block_in_erb - output_buffer = form_tag("http://example.com") { concat "Hello world!" } + output_buffer = form_tag("http://www.example.com") { concat "Hello world!" } - expected = %(<form action="http://example.com" method="post">Hello world!</form>) + expected = whole_form { "Hello world!" } assert_dom_equal expected, output_buffer end def test_form_tag_with_block_and_method_in_erb - output_buffer = form_tag("http://example.com", :method => :put) { concat "Hello world!" } + output_buffer = form_tag("http://www.example.com", :method => :put) { concat "Hello world!" } + + expected = whole_form("http://www.example.com", :method => "put") do + "Hello world!" + end - expected = %(<form action="http://example.com" method="post"><div style='margin:0;padding:0;display:inline'><input type="hidden" name="_method" value="put" /></div>Hello world!</form>) assert_dom_equal expected, output_buffer end diff --git a/actionpack/test/template/html-scanner/sanitizer_test.rb b/actionpack/test/template/html-scanner/sanitizer_test.rb index a6e760b0b6..c9edde8892 100644 --- a/actionpack/test/template/html-scanner/sanitizer_test.rb +++ b/actionpack/test/template/html-scanner/sanitizer_test.rb @@ -257,6 +257,10 @@ class SanitizerTest < ActionController::TestCase assert_sanitized %{<a href=\"http://www.domain.com?var1=1&var2=2\">my link</a>} end + def test_should_sanitize_neverending_attribute + assert_sanitized "<span class=\"\\", "<span class=\"\\\">" + end + protected def assert_sanitized(input, expected = nil) @sanitizer ||= HTML::WhiteListSanitizer.new diff --git a/actionpack/test/template/log_subscriber_test.rb b/actionpack/test/template/log_subscriber_test.rb index 5076dfa70f..eb1e548672 100644 --- a/actionpack/test/template/log_subscriber_test.rb +++ b/actionpack/test/template/log_subscriber_test.rb @@ -1,22 +1,22 @@ require "abstract_unit" -require "rails/log_subscriber/test_helper" -require "action_view/railties/log_subscriber" +require "active_support/log_subscriber/test_helper" +require "action_view/log_subscriber" require "controller/fake_models" class AVLogSubscriberTest < ActiveSupport::TestCase - include Rails::LogSubscriber::TestHelper + include ActiveSupport::LogSubscriber::TestHelper def setup super @old_logger = ActionController::Base.logger @view = ActionView::Base.new(ActionController::Base.view_paths, {}) Rails.stubs(:root).returns(File.expand_path(FIXTURE_LOAD_PATH)) - Rails::LogSubscriber.add(:action_view, ActionView::Railties::LogSubscriber.new) + ActionView::LogSubscriber.attach_to :action_view end def teardown super - Rails::LogSubscriber.log_subscribers.clear + ActiveSupport::LogSubscriber.log_subscribers.clear ActionController::Base.logger = @old_logger end diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb index ec5fe3d1d7..507cdca8d0 100644 --- a/actionpack/test/template/tag_helper_test.rb +++ b/actionpack/test/template/tag_helper_test.rb @@ -95,9 +95,9 @@ class TagHelperTest < ActionView::TestCase assert_equal '1 < 2 & 3', escape_once('1 < 2 & 3') end - def test_double_escaping_attributes + def test_tag_honors_html_safe_for_param_values ['1&2', '1 < 2', '“test“'].each do |escaped| - assert_equal %(<a href="#{escaped}" />), tag('a', :href => escaped) + assert_equal %(<a href="#{escaped}" />), tag('a', :href => escaped.html_safe) end end diff --git a/actionpack/test/template/test_case_test.rb b/actionpack/test/template/test_case_test.rb index 9b50ea8a42..a0c46f8a59 100644 --- a/actionpack/test/template/test_case_test.rb +++ b/actionpack/test/template/test_case_test.rb @@ -37,8 +37,12 @@ module ActionView include SharedTests test_case = self - test "memoizes the _view" do - assert_same _view, _view + test "memoizes the view" do + assert_same view, view + end + + test "exposes view as _view for backwards compatibility" do + assert_same _view, view end test "works without testing a helper module" do @@ -61,13 +65,13 @@ module ActionView end test "delegates notice to request.flash" do - _view.request.flash.expects(:notice).with("this message") - _view.notice("this message") + view.request.flash.expects(:notice).with("this message") + view.notice("this message") end test "delegates alert to request.flash" do - _view.request.flash.expects(:alert).with("this message") - _view.alert("this message") + view.request.flash.expects(:alert).with("this message") + view.alert("this message") end end @@ -136,7 +140,7 @@ module ActionView helper HelperThatInvokesProtectAgainstForgery test "protect_from_forgery? in any helpers returns false" do - assert !_view.help_me + assert !view.help_me end end @@ -200,6 +204,15 @@ module ActionView assert_match /Hello: EloyHello: Manfred/, render(:file => 'test/list') end + test "is able to render partials from templates and also use instance variables after view has been referenced" do + @controller.controller_path = "test" + + view + + @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] + assert_match /Hello: EloyHello: Manfred/, render(:file => 'test/list') + end + end class AssertionsTest < ActionView::TestCase @@ -218,4 +231,26 @@ module ActionView end end end + + class RenderTemplateTest < ActionView::TestCase + test "supports specifying partials" do + controller.controller_path = "test" + render(:template => "test/calling_partial_with_layout") + assert_template :partial => "_partial_for_use_in_layout" + end + + test "supports specifying locals (passing)" do + controller.controller_path = "test" + render(:template => "test/calling_partial_with_layout") + assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "David" } + end + + test "supports specifying locals (failing)" do + controller.controller_path = "test" + render(:template => "test/calling_partial_with_layout") + assert_raise ActiveSupport::TestCase::Assertion, /Somebody else.*David/m do + assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "Somebody Else" } + end + end + end end diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb index d173c5be0a..d22b9fe406 100644 --- a/actionpack/test/template/text_helper_test.rb +++ b/actionpack/test/template/text_helper_test.rb @@ -435,6 +435,7 @@ class TextHelperTest < ActionView::TestCase z39_scheme = 'z39.50r://host:696/db' chrome_scheme = 'chrome://package/section/path' view_source = 'view-source:http://en.wikipedia.org/wiki/URI_scheme' + assert_equal generate_result(file_scheme), auto_link(file_scheme) assert_equal generate_result(z39_scheme), auto_link(z39_scheme) assert_equal generate_result(chrome_scheme), auto_link(chrome_scheme) assert_equal generate_result(view_source), auto_link(view_source) diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 299d6dd5bd..765beebfa3 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -40,19 +40,8 @@ class UrlHelperTest < ActiveSupport::TestCase end alias url_hash hash_for - def test_url_for_escapes_urls + def test_url_for_does_not_escape_urls assert_equal "/?a=b&c=d", url_for(abcd) - assert_equal "/?a=b&c=d", url_for(abcd(:escape => true)) - assert_equal "/?a=b&c=d", url_for(abcd(:escape => false)) - end - - def test_url_for_escaping_is_safety_aware - assert url_for(abcd(:escape => true)).html_safe?, "escaped urls should be html_safe?" - assert !url_for(abcd(:escape => false)).html_safe?, "non-escaped urls should not be html_safe?" - end - - def test_url_for_escapes_url_once - assert_equal "/?a=b&c=d", url_for("/?a=b&c=d") end def test_url_for_with_back @@ -67,11 +56,6 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal 'javascript:history.back()', url_for(:back) end - def test_url_for_from_hash_doesnt_escape_ampersand - path = url_for(hash_for(:foo => :bar, :baz => :quux)) - assert_equal '/?baz=quux&foo=bar', sort_query_string_params(path) - end - # todo: missing test cases def test_button_to_with_straight_url assert_dom_equal "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", button_to("Hello", "http://www.example.com") @@ -81,8 +65,8 @@ class UrlHelperTest < ActiveSupport::TestCase assert_dom_equal "<form method=\"post\" action=\"http://www.example.com/q1=v1&q2=v2\" class=\"button_to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", button_to("Hello", "http://www.example.com/q1=v1&q2=v2") end - def test_button_to_with_escaped_query - assert_dom_equal "<form method=\"post\" action=\"http://www.example.com/q1=v1&q2=v2\" class=\"button_to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", button_to("Hello", "http://www.example.com/q1=v1&q2=v2") + def test_button_to_with_html_safe_URL + assert_dom_equal "<form method=\"post\" action=\"http://www.example.com/q1=v1&q2=v2\" class=\"button_to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", button_to("Hello", "http://www.example.com/q1=v1&q2=v2".html_safe) end def test_button_to_with_query_and_no_name @@ -151,13 +135,12 @@ class UrlHelperTest < ActiveSupport::TestCase def test_link_tag_with_query expected = %{<a href="http://www.example.com?q1=v1&q2=v2">Hello</a>} - assert_dom_equal expected, link_to("Hello", "http://www.example.com?q1=v1&q2=v2") + assert_dom_equal expected, link_to("Hello", "http://www.example.com?q1=v1&q2=v2") end def test_link_tag_with_query_and_no_name - link = link_to(nil, "http://www.example.com?q1=v1&q2=v2") expected = %{<a href="http://www.example.com?q1=v1&q2=v2">http://www.example.com?q1=v1&q2=v2</a>} - assert_dom_equal expected, link + assert_dom_equal expected, link_to(nil, "http://www.example.com?q1=v1&q2=v2") end def test_link_tag_with_back @@ -312,7 +295,7 @@ class UrlHelperTest < ActiveSupport::TestCase @request = request_for_url("/?order=desc&page=1") assert current_page?(hash_for(:order => "desc", :page => "1")) - assert current_page?("http://www.example.com/?order=desc&page=1") + assert current_page?("http://www.example.com/?order=desc&page=1") end def test_link_unless_current @@ -345,7 +328,7 @@ class UrlHelperTest < ActiveSupport::TestCase link_to_unless_current("Showing", "http://www.example.com/?order=asc") @request = request_for_url("/?order=desc") - assert_equal %{<a href="/?order=desc&page=2\">Showing</a>}, + assert_equal %{<a href="/?order=desc&page=2\">Showing</a>}, link_to_unless_current("Showing", hash_for(:order => "desc", :page => 2)) assert_equal %{<a href="http://www.example.com/?order=desc&page=2">Showing</a>}, link_to_unless_current("Showing", "http://www.example.com/?order=desc&page=2") @@ -415,7 +398,7 @@ class UrlHelperTest < ActiveSupport::TestCase private def sort_query_string_params(uri) path, qs = uri.split('?') - qs = qs.split('&').sort.join('&') if qs + qs = qs.split('&').sort.join('&') if qs qs ? "#{path}?#{qs}" : path end end @@ -429,6 +412,10 @@ class UrlHelperControllerTest < ActionController::TestCase map.connect ":controller/:action/:id" # match "/:controller(/:action(/:id))" + + match 'url_helper_controller_test/url_helper/normalize_recall_params', + :to => UrlHelperController.action(:normalize_recall), + :as => :normalize_recall_params end def show_url_for @@ -447,6 +434,14 @@ class UrlHelperControllerTest < ActionController::TestCase render :inline => '<%= url_for(nil) %>' end + def normalize_recall_params + render :inline => '<%= normalize_recall_params_path %>' + end + + def recall_params_not_changed + render :inline => '<%= url_for(:action => :show_url_for) %>' + end + def rescue_action(e) raise e end end @@ -488,6 +483,16 @@ class UrlHelperControllerTest < ActionController::TestCase get :show_named_route, :kind => 'url' assert_equal 'http://testtwo.host/url_helper_controller_test/url_helper/show_named_route', @response.body end + + def test_recall_params_should_be_normalized_when_using_block_route + get :normalize_recall_params + assert_equal '/url_helper_controller_test/url_helper/normalize_recall_params', @response.body + end + + def test_recall_params_should_not_be_changed_when_using_normal_route + get :recall_params_not_changed + assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body + end end class TasksController < ActionController::Base diff --git a/activemodel/README b/activemodel/README index 3945a6da06..6f162ef408 100644 --- a/activemodel/README +++ b/activemodel/README @@ -177,7 +177,8 @@ functionality from the following modules: end - person = Person.new(:first_name => 'zoolander') + person = Person.new + person.first_name = 'zoolander' person.valid? #=> false {Learn more}[link:classes/ActiveModel/Validations.html] diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 1dba664539..1dba664539 100755..100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index a7690ba5b9..817640b178 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -46,7 +46,7 @@ module ActiveModel # end # end # - # Notice that whenever you include ActiveModel::AtributeMethods in your class, + # Notice that whenever you include ActiveModel::AttributeMethods in your class, # it requires you to implement a <tt>attributes</tt> methods which returns a hash # with each attribute name in your model as hash key and the attribute value as # hash value. diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 257644b3fa..e7aad17021 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -2,11 +2,11 @@ require 'active_support/core_ext/array/wrap' require 'active_support/callbacks' module ActiveModel - # == Active Model Call Backs + # == Active Model Callbacks # # Provides an interface for any class to have Active Record like callbacks. # - # Like the Active Record methods, the call back chain is aborted as soon as + # Like the Active Record methods, the callback chain is aborted as soon as # one of the methods in the chain returns false. # # First, extend ActiveModel::Callbacks from the class you are creating: @@ -15,13 +15,13 @@ module ActiveModel # extend ActiveModel::Callbacks # end # - # Then define a list of methods that you want call backs attached to: + # Then define a list of methods that you want callbacks attached to: # # define_model_callbacks :create, :update # # This will provide all three standard callbacks (before, around and after) around # both the :create and :update methods. To implement, you need to wrap the methods - # you want call backs on in a block so that the call backs get a chance to fire: + # you want callbacks on in a block so that the callbacks get a chance to fire: # # def create # _run_create_callbacks do @@ -61,7 +61,7 @@ module ActiveModel # # define_model_callbacks :initializer, :only => :after # - # Note, the <tt>:only => <type></tt> hash will apply to all call backs defined on + # Note, the <tt>:only => <type></tt> hash will apply to all callbacks defined on # that method call. To get around this you can call the define_model_callbacks # method as many times as you need. # @@ -72,8 +72,8 @@ module ActiveModel # Would create +after_create+, +before_update+ and +around_destroy+ methods only. # # You can pass in a class to before_<type>, after_<type> and around_<type>, in which - # case the call back will call that class's <action>_<type> method passing the object - # that the call back is being called on. + # case the callback will call that class's <action>_<type> method passing the object + # that the callback is being called on. # # class MyModel # extend ActiveModel::Callbacks @@ -84,7 +84,7 @@ module ActiveModel # # class AnotherClass # def self.before_create( obj ) - # # obj is the MyModel instance that the call back is being called on + # # obj is the MyModel instance that the callback is being called on # end # end # diff --git a/activemodel/lib/active_model/deprecated_error_methods.rb b/activemodel/lib/active_model/deprecated_error_methods.rb index dd8050c549..adc50773d9 100644 --- a/activemodel/lib/active_model/deprecated_error_methods.rb +++ b/activemodel/lib/active_model/deprecated_error_methods.rb @@ -16,7 +16,7 @@ module ActiveModel end def add_to_base(msg) - ActiveSupport::Deprecation.warn "Errors#add_to_base(msg) has been deprecated, use Errors#[:base] << msg instead" + ActiveSupport::Deprecation.warn "Errors#add_to_base(msg) has been deprecated, use Errors#add(:base, msg) instead" self[:base] << msg end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index a82ce1bee0..4c80863e3a 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -4,7 +4,7 @@ require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' module ActiveModel - # == Active Model Call Backs + # == Active Model Dirty # # Provides a way to track changes in your object in the same way as # Active Record does. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index f4c7400621..d42fc5291d 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/reverse_merge' require 'active_support/ordered_hash' module ActiveModel @@ -61,6 +62,8 @@ module ActiveModel class Errors < ActiveSupport::OrderedHash include DeprecatedErrorMethods + CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank] + # Pass in the instance of the object that is using the errors object. # # class Person @@ -162,15 +165,12 @@ module ActiveModel # # <error>name must be specified</error> # # </errors> def to_xml(options={}) - require 'builder' unless defined? ::Builder - options[:root] ||= "errors" - options[:indent] ||= 2 - options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) - - options[:builder].instruct! unless options.delete(:skip_instruct) - options[:builder].errors do |e| - to_a.each { |error| e.error(error) } - end + to_a.to_xml options.reverse_merge(:root => "errors", :skip_types => true) + end + + # Returns an array as JSON representation for this object. + def as_json(options=nil) + to_a end # Adds +message+ to the error messages on +attribute+, which will be returned on a call to @@ -182,25 +182,44 @@ module ActiveModel # If +message+ is a proc, it will be called, allowing for things like <tt>Time.now</tt> to be used within an error. def add(attribute, message = nil, options = {}) message ||= :invalid - message = generate_message(attribute, message, options) if message.is_a?(Symbol) - message = message.call if message.is_a?(Proc) + + if message.is_a?(Symbol) + message = generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) + elsif message.is_a?(Proc) + message = message.call + end + self[attribute] << message end # Will add an error message to each of the attributes in +attributes+ that is empty. - def add_on_empty(attributes, custom_message = nil) + def add_on_empty(attributes, options = {}) + if options && !options.is_a?(Hash) + options = { :message => options } + ActiveSupport::Deprecation.warn \ + "ActiveModel::Errors#add_on_empty(attributes, custom_message) has been deprecated.\n" + + "Instead of passing a custom_message pass an options Hash { :message => custom_message }." + end + [attributes].flatten.each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) is_empty = value.respond_to?(:empty?) ? value.empty? : false - add(attribute, :empty, :default => custom_message) unless !value.nil? && !is_empty + add(attribute, :empty, options) if value.nil? || is_empty end end # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). - def add_on_blank(attributes, custom_message = nil) + def add_on_blank(attributes, options = {}) + if options && !options.is_a?(Hash) + options = { :message => options } + ActiveSupport::Deprecation.warn \ + "ActiveModel::Errors#add_on_blank(attributes, custom_message) has been deprecated.\n" + + "Instead of passing a custom_message pass an options Hash { :message => custom_message }." + end + [attributes].flatten.each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) - add(attribute, :blank, :default => custom_message) if value.blank? + add(attribute, :blank, options) if value.blank? end end @@ -246,7 +265,7 @@ module ActiveModel # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model name, # translated attribute name and the value are available for interpolation. # - # When using inheritence in your models, it will check all the inherited + # When using inheritance in your models, it will check all the inherited # models too, but only if the model itself hasn't been found. Say you have # <tt>class Admin < User; end</tt> and you wanted the translation for # the <tt>:blank</tt> error +message+ for the <tt>title</tt> +attribute+, @@ -262,24 +281,31 @@ module ActiveModel # <li><tt>errors.attributes.title.blank</tt></li> # <li><tt>errors.messages.blank</tt></li> # </ol> - def generate_message(attribute, message = :invalid, options = {}) - message, options[:default] = options[:default], message if options[:default].is_a?(Symbol) + def generate_message(attribute, type = :invalid, options = {}) + type = options.delete(:message) if options[:message].is_a?(Symbol) + + if options[:default] + ActiveSupport::Deprecation.warn \ + "ActiveModel::Errors#generate_message(attributes, custom_message) has been deprecated.\n" + + "Use ActiveModel::Errors#generate_message(attributes, :message => 'your message') instead." + options[:message] = options.delete(:default) + end defaults = @base.class.lookup_ancestors.map do |klass| - [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{message}", - :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{message}" ] + [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{type}", + :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{type}" ] end - defaults << options.delete(:default) - defaults << :"#{@base.class.i18n_scope}.errors.messages.#{message}" - defaults << :"errors.attributes.#{attribute}.#{message}" - defaults << :"errors.messages.#{message}" + defaults << options.delete(:message) + defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" + defaults << :"errors.attributes.#{attribute}.#{type}" + defaults << :"errors.messages.#{type}" defaults.compact! defaults.flatten! key = defaults.shift - value = @base.send(:read_attribute_for_validation, attribute) + value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil) options = { :default => defaults, diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index a5977bf3cc..fe650053d9 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -56,7 +56,7 @@ module ActiveModel # Returns a boolean that specifies whether the object has been persisted yet. # This is used when calculating the URL for an object. If the object is # not persisted, a form for that object, for instance, will be POSTed to the - # collection. If it is persisted, a form for the object will put PUTed to the + # collection. If it is persisted, a form for the object will be PUT to the # URL for the object. def test_persisted? assert model.respond_to?(:persisted?), "The model should respond to persisted?" @@ -65,7 +65,7 @@ module ActiveModel # == Naming # - # Model.model_name must returns a string with some convenience methods as + # Model.model_name must return a string with some convenience methods as # :human and :partial_path. Check ActiveModel::Naming for more information. # def test_model_naming diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 14f8bf72dc..d0f36ce3b1 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -2,7 +2,6 @@ require 'singleton' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/string/conversions' module ActiveModel module Observing diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 3228cfed9a..0554677296 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -13,7 +13,7 @@ module ActiveModel # extend ActiveModel::Translation # end # - # TranslatedPerson.human_attribute_name('my_attribue') + # TranslatedPerson.human_attribute_name('my_attribute') # #=> "My attribute" # # This also provides the required class methods for hooking into the diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 57487cf75a..5779ba3b29 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/keys' require 'active_model/errors' +require 'active_model/validations/callbacks' module ActiveModel @@ -91,7 +92,7 @@ module ActiveModel end # Adds a validation method or block to the class. This is useful when - # overriding the +validate+ instance method becomes too unwieldly and + # overriding the +validate+ instance method becomes too unwieldy and # you're looking for more descriptive declaration of your validations. # # This can be done with a symbol pointing to a method: @@ -164,8 +165,7 @@ module ActiveModel def valid?(context = nil) current_context, self.validation_context = validation_context, context errors.clear - _run_validate_callbacks - errors.empty? + run_validations! ensure self.validation_context = current_context end @@ -176,7 +176,7 @@ module ActiveModel !valid?(context) end - # Hook method defining how an attribute value should be retieved. By default + # Hook method defining how an attribute value should be retrieved. By default # this is assumed to be an instance named after the attribute. Override this # method in subclasses should you need to retrieve the value for a given # attribute differently: @@ -194,6 +194,13 @@ module ActiveModel # end # alias :read_attribute_for_validation :send + + protected + + def run_validations! + _run_validate_callbacks + errors.empty? + end end end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 26b12b504b..99b8966def 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -1,4 +1,6 @@ module ActiveModel + + # == Active Model Acceptance Validator module Validations class AcceptanceValidator < EachValidator def initialize(options) @@ -7,10 +9,10 @@ module ActiveModel def validate_each(record, attribute, value) unless value == options[:accept] - record.errors.add(attribute, :accepted, :default => options[:message]) + record.errors.add(attribute, :accepted, options.except(:accept, :allow_nil)) end end - + def setup(klass) # Note: instance_methods.map(&:to_s) is important for 1.9 compatibility # as instance_methods returns symbols unlike 1.8 which returns strings. @@ -22,29 +24,42 @@ module ActiveModel end module HelperMethods - # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: + # Encapsulates the pattern of wanting to validate the acceptance of a + # terms of service check box (or similar agreement). Example: # # class Person < ActiveRecord::Base # validates_acceptance_of :terms_of_service # validates_acceptance_of :eula, :message => "must be abided" # end # - # If the database column does not exist, the +terms_of_service+ attribute is entirely virtual. This check is - # performed only if +terms_of_service+ is not +nil+ and by default on save. + # If the database column does not exist, the +terms_of_service+ attribute + # is entirely virtual. This check is performed only if +terms_of_service+ + # is not +nil+ and by default on save. # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "must be accepted"). - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is true). - # * <tt>:accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which - # makes it easy to relate to an HTML checkbox. This should be set to +true+ if you are validating a database - # column, since the attribute is typecast from "1" to +true+ before validation. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. + # * <tt>:message</tt> - A custom error message (default is: "must be + # accepted"). + # * <tt>:on</tt> - Specifies when this validation is active (default is + # <tt>:save</tt>, other options are <tt>:create</tt> and + # <tt>:update</tt>). + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default + # is true). + # * <tt>:accept</tt> - Specifies value that is considered accepted. + # The default value is a string "1", which makes it easy to relate to + # an HTML checkbox. This should be set to +true+ if you are validating + # a database column, since the attribute is typecast from "1" to +true+ + # before validation. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, + # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The + # method, proc or string should return or evaluate to a true or false + # value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to + # determine if the validation should not occur (for example, + # <tt>:unless => :skip_validation</tt>, or + # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). + # The method, proc or string should return or evaluate to a true or + # false value. def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb new file mode 100644 index 0000000000..afd65d3dd5 --- /dev/null +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -0,0 +1,57 @@ +require 'active_support/callbacks' + +module ActiveModel + module Validations + module Callbacks + # == Active Model Validation callbacks + # + # Provides an interface for any class to have <tt>before_validation</tt> and + # <tt>after_validation</tt> callbacks. + # + # First, extend ActiveModel::Callbacks from the class you are creating: + # + # class MyModel + # include ActiveModel::Validations::Callbacks + # + # before_validation :do_stuff_before_validation + # after_validation :do_tuff_after_validation + # end + # + # Like other before_* callbacks if <tt>before_validation</tt> returns false + # then <tt>valid?</tt> will not be called. + extend ActiveSupport::Concern + + included do + include ActiveSupport::Callbacks + define_callbacks :validation, :terminator => "result == false", :scope => [:kind, :name] + end + + module ClassMethods + def before_validation(*args, &block) + options = args.last + if options.is_a?(Hash) && options[:on] + options[:if] = Array.wrap(options[:if]) + options[:if] << "self.validation_context == :#{options[:on]}" + end + set_callback(:validation, :before, *args, &block) + end + + def after_validation(*args, &block) + options = args.extract_options! + options[:prepend] = true + options[:if] = Array.wrap(options[:if]) + options[:if] << "!halted && value != false" + options[:if] << "self.validation_context == :#{options[:on]}" if options[:on] + set_callback(:validation, :after, *(args << options), &block) + end + end + + protected + + # Overwrite run validations to include callbacks. + def run_validations! + _run_validation_callbacks { super } + end + end + end +end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 51445343f2..3a80893866 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -1,45 +1,59 @@ module ActiveModel + + # == Active Model Confirmation Validator module Validations class ConfirmationValidator < EachValidator def validate_each(record, attribute, value) confirmed = record.send(:"#{attribute}_confirmation") return if confirmed.nil? || value == confirmed - record.errors.add(attribute, :confirmation, :default => options[:message]) + record.errors.add(attribute, :confirmation, options) end - + def setup(klass) - klass.send(:attr_accessor, *attributes.map { |attribute| :"#{attribute}_confirmation" }) + klass.send(:attr_accessor, *attributes.map { |attribute| :"#{attribute}_confirmation" }) end end module HelperMethods - # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: + # Encapsulates the pattern of wanting to validate a password or email + # address field with a confirmation. For example: # # Model: # class Person < ActiveRecord::Base # validates_confirmation_of :user_name, :password - # validates_confirmation_of :email_address, :message => "should match confirmation" + # validates_confirmation_of :email_address, + # :message => "should match confirmation" # end # # View: # <%= password_field "person", "password" %> # <%= password_field "person", "password_confirmation" %> # - # The added +password_confirmation+ attribute is virtual; it exists only as an in-memory attribute for validating the password. - # To achieve this, the validation adds accessors to the model for the confirmation attribute. NOTE: This check is performed - # only if +password_confirmation+ is not +nil+, and by default only on save. To require confirmation, make sure to add a presence - # check for the confirmation attribute: + # The added +password_confirmation+ attribute is virtual; it exists only + # as an in-memory attribute for validating the password. To achieve this, + # the validation adds accessors to the model for the confirmation + # attribute. + # + # NOTE: This check is performed only if +password_confirmation+ is not + # +nil+, and by default only on save. To require confirmation, make sure + # to add a presence check for the confirmation attribute: # # validates_presence_of :password_confirmation, :if => :password_changed? # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation"). - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # * <tt>:message</tt> - A custom error message (default is: "doesn't match + # confirmation"). + # * <tt>:on</tt> - Specifies when this validation is active (default is + # <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, + # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The + # method, proc or string should return or evaluate to a true or false + # value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to + # determine if the validation should not occur (e.g. + # <tt>:unless => :skip_validation</tt>, or + # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_confirmation_of(*attr_names) validates_with ConfirmationValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 2ee78f5dd2..4138892786 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,4 +1,6 @@ module ActiveModel + + # == Active Model Exclusion Validator module Validations class ExclusionValidator < EachValidator def check_validity! @@ -7,8 +9,9 @@ module ActiveModel end def validate_each(record, attribute, value) - return unless options[:in].include?(value) - record.errors.add(attribute, :exclusion, :default => options[:message], :value => value) + if options[:in].include?(value) + record.errors.add(attribute, :exclusion, options.except(:in).merge!(:value => value)) + end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index c34c860d4d..104f403492 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -1,14 +1,16 @@ module ActiveModel + + # == Active Model Format Validator module Validations class FormatValidator < EachValidator def validate_each(record, attribute, value) if options[:with] && value.to_s !~ options[:with] - record.errors.add(attribute, :invalid, :default => options[:message], :value => value) + record.errors.add(attribute, :invalid, options.except(:with).merge!(:value => value)) elsif options[:without] && value.to_s =~ options[:without] - record.errors.add(attribute, :invalid, :default => options[:message], :value => value) + record.errors.add(attribute, :invalid, options.except(:without).merge!(:value => value)) end end - + def check_validity! unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or" raise ArgumentError, "Either :with or :without must be supplied (but not both)" diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 446646d247..049b093618 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,4 +1,6 @@ module ActiveModel + + # == Active Model Inclusion Validator module Validations class InclusionValidator < EachValidator def check_validity! @@ -7,8 +9,9 @@ module ActiveModel end def validate_each(record, attribute, value) - return if options[:in].include?(value) - record.errors.add(attribute, :inclusion, :default => options[:message], :value => value) + unless options[:in].include?(value) + record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) + end end end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index d7218f4f52..c8a77ad666 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,10 +1,13 @@ module ActiveModel + + # == Active Model Length Validator module Validations class LengthValidator < EachValidator MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze DEFAULT_TOKENIZER = lambda { |value| value.split(//) } + RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long] def initialize(options) if range = (options.delete(:in) || options.delete(:within)) @@ -37,7 +40,8 @@ module ActiveModel CHECKS.each do |key, validity_check| next unless check_value = options[key] - custom_message = options[:message] || options[MESSAGES[key]] + default_message = options[MESSAGES[key]] + options[:message] ||= default_message if default_message valid_value = if key == :maximum value.nil? || value.size.send(validity_check, check_value) @@ -46,7 +50,9 @@ module ActiveModel end next if valid_value - record.errors.add(attribute, MESSAGES[key], :default => custom_message, :count => check_value) + + record.errors.add(attribute, MESSAGES[key], + options.except(*RESERVED_OPTIONS).merge!(:count => check_value)) end end end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 062b4cd17f..b6aff7aa6b 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -1,10 +1,14 @@ module ActiveModel + + # == Active Model Numericality Validator module Validations class NumericalityValidator < EachValidator CHECKS = { :greater_than => :>, :greater_than_or_equal_to => :>=, :equal_to => :==, :less_than => :<, :less_than_or_equal_to => :<=, :odd => :odd?, :even => :even? }.freeze + RESERVED_OPTIONS = CHECKS.keys + [:only_integer] + def initialize(options) super(options.reverse_merge(:only_integer => false, :allow_nil => false)) end @@ -26,13 +30,13 @@ module ActiveModel return if options[:allow_nil] && raw_value.nil? unless value = parse_raw_value_as_a_number(raw_value) - record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => options[:message]) + record.errors.add(attr_name, :not_a_number, filtered_options(raw_value)) return end if options[:only_integer] unless value = parse_raw_value_as_an_integer(raw_value) - record.errors.add(attr_name, :not_an_integer, :value => raw_value, :default => options[:message]) + record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value)) return end end @@ -41,14 +45,14 @@ module ActiveModel case option when :odd, :even unless value.to_i.send(CHECKS[option]) - record.errors.add(attr_name, option, :value => value, :default => options[:message]) + record.errors.add(attr_name, option, filtered_options(value)) end else option_value = option_value.call(record) if option_value.is_a?(Proc) option_value = record.send(option_value) if option_value.is_a?(Symbol) unless value.send(CHECKS[option], option_value) - record.errors.add(attr_name, option, :default => options[:message], :value => value, :count => option_value) + record.errors.add(attr_name, option, filtered_options(value).merge(:count => option_value)) end end end @@ -73,6 +77,9 @@ module ActiveModel raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/ end + def filtered_options(value) + options.except(*RESERVED_OPTIONS).merge!(:value => value) + end end module HelperMethods diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index b319f4834b..28c4640b17 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -1,10 +1,12 @@ require 'active_support/core_ext/object/blank' module ActiveModel + + # == Active Model Presence Validator module Validations class PresenceValidator < EachValidator def validate(record) - record.errors.add_on_blank(attributes, options[:message]) + record.errors.add_on_blank(attributes, options) end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 90b244228a..0674640925 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -1,6 +1,8 @@ require 'active_support/core_ext/hash/slice' module ActiveModel + + # == Active Model validates method module Validations module ClassMethods # This method is a shortcut to all default validators and any custom @@ -20,7 +22,7 @@ module ActiveModel # validates :username, :presence => true # validates :username, :uniqueness => true # - # The power of the +validates+ method comes when using cusom validators + # The power of the +validates+ method comes when using custom validators # and default validators in one call for a given attribute e.g. # # class EmailValidator < ActiveModel::EachValidator diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 6dbde5bfad..200efd4eb5 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -9,7 +9,6 @@ module ActiveModel end module ClassMethods - # Passes the record off to the class or classes specified and allows them # to add errors based on more complex conditions. # diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index dd9e39843e..689c617177 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -109,7 +109,7 @@ module ActiveModel #:nodoc: @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous? end - # Accepts options that will be made availible through the +options+ reader. + # Accepts options that will be made available through the +options+ reader. def initialize(options) @options = options end @@ -155,7 +155,7 @@ module ActiveModel #:nodoc: end end - # Override this method in subclasses with the validation logic, adding + # Override this method in subclasses with the validation logic, adding # errors to the records +errors+ array where necessary. def validate_each(record, attribute, value) raise NotImplementedError diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index a81584bbad..a32f11484a 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -6,6 +6,7 @@ $:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) require 'config' require 'active_model' require 'active_support/core_ext/string/access' +require 'active_support/core_ext/hash/except' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb new file mode 100644 index 0000000000..1cf09758f9 --- /dev/null +++ b/activemodel/test/cases/validations/callbacks_test.rb @@ -0,0 +1,78 @@ +# encoding: utf-8 +require 'cases/helper' + +class Dog + include ActiveModel::Validations + include ActiveModel::Validations::Callbacks + + attr_accessor :name + attr_writer :history + + def history + @history ||= [] + end +end + +class DogWithMethodCallbacks < Dog + before_validation :set_before_validation_marker + after_validation :set_after_validation_marker + + def set_before_validation_marker; self.history << 'before_validation_marker'; end + def set_after_validation_marker; self.history << 'after_validation_marker' ; end +end + +class DogValidtorsAreProc < Dog + before_validation { self.history << 'before_validation_marker' } + after_validation { self.history << 'after_validation_marker' } +end + +class DogWithTwoValidators < Dog + before_validation { self.history << 'before_validation_marker1' } + before_validation { self.history << 'before_validation_marker2' } +end + +class DogValidatorReturningFalse < Dog + before_validation { false } + before_validation { self.history << 'before_validation_marker2' } +end + +class DogWithMissingName < Dog + before_validation { self.history << 'before_validation_marker' } + validates_presence_of :name +end + +class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase + + def test_before_validation_and_after_validation_callbacks_should_be_called + d = DogWithMethodCallbacks.new + d.valid? + assert_equal ['before_validation_marker', 'after_validation_marker'], d.history + end + + def test_before_validation_and_after_validation_callbacks_should_be_called_with_proc + d = DogValidtorsAreProc.new + d.valid? + assert_equal ['before_validation_marker', 'after_validation_marker'], d.history + end + + def test_before_validation_and_after_validation_callbacks_should_be_called_in_declared_order + d = DogWithTwoValidators.new + d.valid? + assert_equal ['before_validation_marker1', 'before_validation_marker2'], d.history + end + + def test_further_callbacks_should_not_be_called_if_before_validation_returns_false + d = DogValidatorReturningFalse.new + output = d.valid? + assert_equal [], d.history + assert_equal false, output + end + + def test_validation_test_should_be_done + d = DogWithMissingName.new + output = d.valid? + assert_equal ['before_validation_marker'], d.history + assert_equal false, output + end + +end diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb index 58a8d179ad..0679e67f84 100644 --- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -8,131 +8,131 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase @person = Person.new end - # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) + # validates_inclusion_of: generate_message(attr_name, :inclusion, :message => custom_message, :value => value) def test_generate_message_inclusion_with_default_message - assert_equal 'is not included in the list', @person.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title') + assert_equal 'is not included in the list', @person.errors.generate_message(:title, :inclusion, :value => 'title') end def test_generate_message_inclusion_with_custom_message - assert_equal 'custom message title', @person.errors.generate_message(:title, :inclusion, :default => 'custom message %{value}', :value => 'title') + assert_equal 'custom message title', @person.errors.generate_message(:title, :inclusion, :message => 'custom message %{value}', :value => 'title') end - # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) + # validates_exclusion_of: generate_message(attr_name, :exclusion, :message => custom_message, :value => value) def test_generate_message_exclusion_with_default_message - assert_equal 'is reserved', @person.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title') + assert_equal 'is reserved', @person.errors.generate_message(:title, :exclusion, :value => 'title') end def test_generate_message_exclusion_with_custom_message - assert_equal 'custom message title', @person.errors.generate_message(:title, :exclusion, :default => 'custom message %{value}', :value => 'title') + assert_equal 'custom message title', @person.errors.generate_message(:title, :exclusion, :message => 'custom message %{value}', :value => 'title') end - # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + # validates_format_of: generate_message(attr_name, :invalid, :message => custom_message, :value => value) def test_generate_message_invalid_with_default_message - assert_equal 'is invalid', @person.errors.generate_message(:title, :invalid, :default => nil, :value => 'title') + assert_equal 'is invalid', @person.errors.generate_message(:title, :invalid, :value => 'title') end def test_generate_message_invalid_with_custom_message - assert_equal 'custom message title', @person.errors.generate_message(:title, :invalid, :default => 'custom message %{value}', :value => 'title') + assert_equal 'custom message title', @person.errors.generate_message(:title, :invalid, :message => 'custom message %{value}', :value => 'title') end - # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message]) + # validates_confirmation_of: generate_message(attr_name, :confirmation, :message => custom_message) def test_generate_message_confirmation_with_default_message - assert_equal "doesn't match confirmation", @person.errors.generate_message(:title, :confirmation, :default => nil) + assert_equal "doesn't match confirmation", @person.errors.generate_message(:title, :confirmation) end def test_generate_message_confirmation_with_custom_message - assert_equal 'custom message', @person.errors.generate_message(:title, :confirmation, :default => 'custom message') + assert_equal 'custom message', @person.errors.generate_message(:title, :confirmation, :message => 'custom message') end - # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message]) + # validates_acceptance_of: generate_message(attr_name, :accepted, :message => custom_message) def test_generate_message_accepted_with_default_message - assert_equal "must be accepted", @person.errors.generate_message(:title, :accepted, :default => nil) + assert_equal "must be accepted", @person.errors.generate_message(:title, :accepted) end def test_generate_message_accepted_with_custom_message - assert_equal 'custom message', @person.errors.generate_message(:title, :accepted, :default => 'custom message') + assert_equal 'custom message', @person.errors.generate_message(:title, :accepted, :message => 'custom message') end - # add_on_empty: generate_message(attr, :empty, :default => custom_message) + # add_on_empty: generate_message(attr, :empty, :message => custom_message) def test_generate_message_empty_with_default_message - assert_equal "can't be empty", @person.errors.generate_message(:title, :empty, :default => nil) + assert_equal "can't be empty", @person.errors.generate_message(:title, :empty) end def test_generate_message_empty_with_custom_message - assert_equal 'custom message', @person.errors.generate_message(:title, :empty, :default => 'custom message') + assert_equal 'custom message', @person.errors.generate_message(:title, :empty, :message => 'custom message') end - # add_on_blank: generate_message(attr, :blank, :default => custom_message) + # add_on_blank: generate_message(attr, :blank, :message => custom_message) def test_generate_message_blank_with_default_message - assert_equal "can't be blank", @person.errors.generate_message(:title, :blank, :default => nil) + assert_equal "can't be blank", @person.errors.generate_message(:title, :blank) end def test_generate_message_blank_with_custom_message - assert_equal 'custom message', @person.errors.generate_message(:title, :blank, :default => 'custom message') + assert_equal 'custom message', @person.errors.generate_message(:title, :blank, :message => 'custom message') end - # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) + # validates_length_of: generate_message(attr, :too_long, :message => custom_message, :count => option_value.end) def test_generate_message_too_long_with_default_message - assert_equal "is too long (maximum is 10 characters)", @person.errors.generate_message(:title, :too_long, :default => nil, :count => 10) + assert_equal "is too long (maximum is 10 characters)", @person.errors.generate_message(:title, :too_long, :count => 10) end def test_generate_message_too_long_with_custom_message - assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, :default => 'custom message %{count}', :count => 10) + assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, :message => 'custom message %{count}', :count => 10) end - # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) + # validates_length_of: generate_message(attr, :too_short, :default => custom_message, :count => option_value.begin) def test_generate_message_too_short_with_default_message - assert_equal "is too short (minimum is 10 characters)", @person.errors.generate_message(:title, :too_short, :default => nil, :count => 10) + assert_equal "is too short (minimum is 10 characters)", @person.errors.generate_message(:title, :too_short, :count => 10) end def test_generate_message_too_short_with_custom_message - assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, :default => 'custom message %{count}', :count => 10) + assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, :message => 'custom message %{count}', :count => 10) end - # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value) + # validates_length_of: generate_message(attr, :wrong_length, :message => custom_message, :count => option_value) def test_generate_message_wrong_length_with_default_message - assert_equal "is the wrong length (should be 10 characters)", @person.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10) + assert_equal "is the wrong length (should be 10 characters)", @person.errors.generate_message(:title, :wrong_length, :count => 10) end def test_generate_message_wrong_length_with_custom_message - assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, :default => 'custom message %{count}', :count => 10) + assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, :message => 'custom message %{count}', :count => 10) end - # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) + # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :message => custom_message) def test_generate_message_not_a_number_with_default_message - assert_equal "is not a number", @person.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title') + assert_equal "is not a number", @person.errors.generate_message(:title, :not_a_number, :value => 'title') end def test_generate_message_not_a_number_with_custom_message - assert_equal 'custom message title', @person.errors.generate_message(:title, :not_a_number, :default => 'custom message %{value}', :value => 'title') + assert_equal 'custom message title', @person.errors.generate_message(:title, :not_a_number, :message => 'custom message %{value}', :value => 'title') end - # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) + # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => custom_message) def test_generate_message_greater_than_with_default_message - assert_equal "must be greater than 10", @person.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10) + assert_equal "must be greater than 10", @person.errors.generate_message(:title, :greater_than, :value => 'title', :count => 10) end def test_generate_message_greater_than_or_equal_to_with_default_message - assert_equal "must be greater than or equal to 10", @person.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + assert_equal "must be greater than or equal to 10", @person.errors.generate_message(:title, :greater_than_or_equal_to, :value => 'title', :count => 10) end def test_generate_message_equal_to_with_default_message - assert_equal "must be equal to 10", @person.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10) + assert_equal "must be equal to 10", @person.errors.generate_message(:title, :equal_to, :value => 'title', :count => 10) end def test_generate_message_less_than_with_default_message - assert_equal "must be less than 10", @person.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10) + assert_equal "must be less than 10", @person.errors.generate_message(:title, :less_than, :value => 'title', :count => 10) end def test_generate_message_less_than_or_equal_to_with_default_message - assert_equal "must be less than or equal to 10", @person.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + assert_equal "must be less than or equal to 10", @person.errors.generate_message(:title, :less_than_or_equal_to, :value => 'title', :count => 10) end def test_generate_message_odd_with_default_message - assert_equal "must be odd", @person.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10) + assert_equal "must be odd", @person.errors.generate_message(:title, :odd, :value => 'title', :count => 10) end def test_generate_message_even_with_default_message - assert_equal "must be even", @person.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10) + assert_equal "must be even", @person.errors.generate_message(:title, :even, :value => 'title', :count => 10) end end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 547d80f46e..e9f0e430fe 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -22,23 +22,23 @@ class I18nValidationTest < ActiveModel::TestCase end def test_errors_add_on_empty_generates_message - @person.errors.expects(:generate_message).with(:title, :empty, {:default => nil}) + @person.errors.expects(:generate_message).with(:title, :empty, {}) @person.errors.add_on_empty :title end def test_errors_add_on_empty_generates_message_with_custom_default_message - @person.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'}) - @person.errors.add_on_empty :title, 'custom' + @person.errors.expects(:generate_message).with(:title, :empty, {:message => 'custom'}) + @person.errors.add_on_empty :title, :message => 'custom' end def test_errors_add_on_blank_generates_message - @person.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) + @person.errors.expects(:generate_message).with(:title, :blank, {}) @person.errors.add_on_blank :title end def test_errors_add_on_blank_generates_message_with_custom_default_message - @person.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) - @person.errors.add_on_blank :title, 'custom' + @person.errors.expects(:generate_message).with(:title, :blank, {:message => 'custom'}) + @person.errors.add_on_blank :title, :message => 'custom' end def test_full_message_encoding @@ -62,441 +62,272 @@ class I18nValidationTest < ActiveModel::TestCase end # ActiveModel::Validations + + # A set of common cases for ActiveModel::Validations message generation that + # are used to generate tests to keep things DRY + # + COMMON_CASES = [ + # [ case, validation_options, generate_message_options] + [ "given no options", {}, {}], + [ "given custom message", {:message => "custom"}, {:message => "custom"}], + [ "given if condition", {:if => lambda { true }}, {}], + [ "given unless condition", {:unless => lambda { false }}, {}], + [ "given option that is not reserved", {:format => "jpg"}, {:format => "jpg" }] + ] + # validates_confirmation_of w/ mocha - def test_validates_confirmation_of_generates_message - Person.validates_confirmation_of :title - @person.title_confirmation = 'foo' - @person.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil}) - @person.valid? - end - def test_validates_confirmation_of_generates_message_with_custom_default_message - Person.validates_confirmation_of :title, :message => 'custom' - @person.title_confirmation = 'foo' - @person.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_confirmation_of on generated message #{name}" do + Person.validates_confirmation_of :title, validation_options + @person.title_confirmation = 'foo' + @person.errors.expects(:generate_message).with(:title, :confirmation, generate_message_options) + @person.valid? + end end # validates_acceptance_of w/ mocha - def test_validates_acceptance_of_generates_message - Person.validates_acceptance_of :title, :allow_nil => false - @person.errors.expects(:generate_message).with(:title, :accepted, {:default => nil}) - @person.valid? - end - - def test_validates_acceptance_of_generates_message_with_custom_default_message - Person.validates_acceptance_of :title, :message => 'custom', :allow_nil => false - @person.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_acceptance_of on generated message #{name}" do + Person.validates_acceptance_of :title, validation_options.merge(:allow_nil => false) + @person.errors.expects(:generate_message).with(:title, :accepted, generate_message_options) + @person.valid? + end end # validates_presence_of w/ mocha - def test_validates_presence_of_generates_message - Person.validates_presence_of :title - @person.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) - @person.valid? - end - - def test_validates_presence_of_generates_message_with_custom_default_message - Person.validates_presence_of :title, :message => 'custom' - @person.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_presence_of on generated message #{name}" do + Person.validates_presence_of :title, validation_options + @person.errors.expects(:generate_message).with(:title, :blank, generate_message_options) + @person.valid? + end end - # validates_length_of :within w/ mocha + # validates_length_of :within too short w/ mocha - def test_validates_length_of_within_generates_message_with_title_too_short - Person.validates_length_of :title, :within => 3..5 - @person.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_length_of for :withing on generated message when too short #{name}" do + Person.validates_length_of :title, validation_options.merge(:within => 3..5) + @person.errors.expects(:generate_message).with(:title, :too_short, generate_message_options.merge(:count => 3)) + @person.valid? + end end - def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message - Person.validates_length_of :title, :within => 3..5, :too_short => 'custom' - @person.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) - @person.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_long - Person.validates_length_of :title, :within => 3..5 - @person.title = 'this title is too long' - @person.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) - @person.valid? - end + # validates_length_of :within too long w/ mocha - def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message - Person.validates_length_of :title, :within => 3..5, :too_long => 'custom' - @person.title = 'this title is too long' - @person.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_length_of for :too_long generated message #{name}" do + Person.validates_length_of :title, validation_options.merge(:within => 3..5) + @person.title = 'this title is too long' + @person.errors.expects(:generate_message).with(:title, :too_long, generate_message_options.merge(:count => 5)) + @person.valid? + end end # validates_length_of :is w/ mocha - def test_validates_length_of_is_generates_message - Person.validates_length_of :title, :is => 5 - @person.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil}) - @person.valid? - end - - def test_validates_length_of_is_generates_message_with_custom_default_message - Person.validates_length_of :title, :is => 5, :message => 'custom' - @person.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_length_of for :is on generated message #{name}" do + Person.validates_length_of :title, validation_options.merge(:is => 5) + @person.errors.expects(:generate_message).with(:title, :wrong_length, generate_message_options.merge(:count => 5)) + @person.valid? + end end # validates_format_of w/ mocha - def test_validates_format_of_generates_message - Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ - @person.title = '72x' - @person.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil}) - @person.valid? - end - - def test_validates_format_of_generates_message_with_custom_default_message - Person.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom' - @person.title = '72x' - @person.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_format_of on generated message #{name}" do + Person.validates_format_of :title, validation_options.merge(:with => /^[1-9][0-9]*$/) + @person.title = '72x' + @person.errors.expects(:generate_message).with(:title, :invalid, generate_message_options.merge(:value => '72x')) + @person.valid? + end end # validates_inclusion_of w/ mocha - def test_validates_inclusion_of_generates_message - Person.validates_inclusion_of :title, :in => %w(a b c) - @person.title = 'z' - @person.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil}) - @person.valid? - end - - def test_validates_inclusion_of_generates_message_with_custom_default_message - Person.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom' - @person.title = 'z' - @person.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_inclusion_of on generated message #{name}" do + Person.validates_inclusion_of :title, validation_options.merge(:in => %w(a b c)) + @person.title = 'z' + @person.errors.expects(:generate_message).with(:title, :inclusion, generate_message_options.merge(:value => 'z')) + @person.valid? + end end # validates_exclusion_of w/ mocha - def test_validates_exclusion_of_generates_message - Person.validates_exclusion_of :title, :in => %w(a b c) - @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil}) - @person.valid? - end - - def test_validates_exclusion_of_generates_message_with_custom_default_message - Person.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom' - @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_exclusion_of generated message #{name}" do + Person.validates_exclusion_of :title, validation_options.merge(:in => %w(a b c)) + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :exclusion, generate_message_options.merge(:value => 'a')) + @person.valid? + end end # validates_numericality_of without :only_integer w/ mocha - def test_validates_numericality_of_generates_message - Person.validates_numericality_of :title - @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) - @person.valid? - end - - def test_validates_numericality_of_generates_message_with_custom_default_message - Person.validates_numericality_of :title, :message => 'custom' - @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_numericality_of generated message #{name}" do + Person.validates_numericality_of :title, validation_options + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :not_a_number, generate_message_options.merge(:value => 'a')) + @person.valid? + end end # validates_numericality_of with :only_integer w/ mocha - def test_validates_numericality_of_only_integer_generates_message - Person.validates_numericality_of :title, :only_integer => true - @person.title = '0.0' - @person.errors.expects(:generate_message).with(:title, :not_an_integer, {:value => '0.0', :default => nil}) - @person.valid? - end - - def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message - Person.validates_numericality_of :title, :only_integer => true, :message => 'custom' - @person.title = '0.0' - @person.errors.expects(:generate_message).with(:title, :not_an_integer, {:value => '0.0', :default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_numericality_of for :only_integer on generated message #{name}" do + Person.validates_numericality_of :title, validation_options.merge(:only_integer => true) + @person.title = '0.0' + @person.errors.expects(:generate_message).with(:title, :not_an_integer, generate_message_options.merge(:value => '0.0')) + @person.valid? + end end # validates_numericality_of :odd w/ mocha - def test_validates_numericality_of_odd_generates_message - Person.validates_numericality_of :title, :only_integer => true, :odd => true - @person.title = 0 - @person.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil}) - @person.valid? - end - - def test_validates_numericality_of_odd_generates_message_with_custom_default_message - Person.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom' - @person.title = 0 - @person.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_numericality_of for :odd on generated message #{name}" do + Person.validates_numericality_of :title, validation_options.merge(:only_integer => true, :odd => true) + @person.title = 0 + @person.errors.expects(:generate_message).with(:title, :odd, generate_message_options.merge(:value => 0)) + @person.valid? + end end # validates_numericality_of :less_than w/ mocha - def test_validates_numericality_of_less_than_generates_message - Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 - @person.title = 1 - @person.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil}) - @person.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_numericality_of for :less_than on generated message #{name}" do + Person.validates_numericality_of :title, validation_options.merge(:only_integer => true, :less_than => 0) + @person.title = 1 + @person.errors.expects(:generate_message).with(:title, :less_than, generate_message_options.merge(:value => 1, :count => 0)) + @person.valid? + end end - def test_validates_numericality_of_less_than_odd_generates_message_with_custom_default_message - Person.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom' - @person.title = 1 - @person.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'}) - @person.valid? - end - # validates_confirmation_of w/o mocha + # To make things DRY this macro is defined to define 3 tests for every validation case. + def self.set_expectations_for_validation(validation, error_type, &block_that_sets_validation) + # test "validates_confirmation_of finds custom model key translation when blank" + test "#{validation} finds custom model key translation when #{error_type}" do + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {error_type => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}} - def test_validates_confirmation_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:confirmation => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:confirmation => 'global message'}} + yield(@person, {}) + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end - Person.validates_confirmation_of :title - @person.title_confirmation = 'foo' - @person.valid? - assert_equal ['custom message'], @person.errors[:title] - end + # test "validates_confirmation_of finds custom model key translation with interpolation when blank" + test "#{validation} finds custom model key translation with interpolation when #{error_type}" do + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {error_type => 'custom message with %{extra}'}}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}} - def test_validates_confirmation_of_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:confirmation => 'global message'}} + yield(@person, {:extra => "extra information"}) + @person.valid? + assert_equal ['custom message with extra information'], @person.errors[:title] + end - Person.validates_confirmation_of :title - @person.title_confirmation = 'foo' - @person.valid? - assert_equal ['global message'], @person.errors[:title] - end + # test "validates_confirmation_of finds global default key translation when blank" + test "#{validation} finds global default key translation when #{error_type}" do + I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}} - # validates_acceptance_of w/o mocha - - def test_validates_acceptance_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:accepted => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:accepted => 'global message'}} - - Person.validates_acceptance_of :title, :allow_nil => false - @person.valid? - assert_equal ['custom message'], @person.errors[:title] + yield(@person, {}) + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end end - def test_validates_acceptance_of_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:accepted => 'global message'}} + # validates_confirmation_of w/o mocha - Person.validates_acceptance_of :title, :allow_nil => false - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_confirmation_of", :confirmation do |person, options_to_merge| + Person.validates_confirmation_of :title, options_to_merge + person.title_confirmation = 'foo' end - # validates_presence_of w/o mocha - - def test_validates_presence_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:blank => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:blank => 'global message'}} + # validates_acceptance_of w/o mocha - Person.validates_presence_of :title - @person.valid? - assert_equal ['custom message'], @person.errors[:title] + set_expectations_for_validation "validates_acceptance_of", :accepted do |person, options_to_merge| + Person.validates_acceptance_of :title, options_to_merge.merge(:allow_nil => false) end - def test_validates_presence_of_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:blank => 'global message'}} + # validates_presence_of w/o mocha - Person.validates_presence_of :title - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_presence_of", :blank do |person, options_to_merge| + Person.validates_presence_of :title, options_to_merge end # validates_length_of :within w/o mocha - def test_validates_length_of_within_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:too_short => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:too_short => 'global message'}} - - Person.validates_length_of :title, :within => 3..5 - @person.valid? - assert_equal ['custom message'], @person.errors[:title] + set_expectations_for_validation "validates_length_of", :too_short do |person, options_to_merge| + Person.validates_length_of :title, options_to_merge.merge(:within => 3..5) end - def test_validates_length_of_within_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:too_short => 'global message'}} - - Person.validates_length_of :title, :within => 3..5 - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_length_of", :too_long do |person, options_to_merge| + Person.validates_length_of :title, options_to_merge.merge(:within => 3..5) + person.title = "too long" end # validates_length_of :is w/o mocha - def test_validates_length_of_is_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:wrong_length => 'global message'}} - - Person.validates_length_of :title, :is => 5 - @person.valid? - assert_equal ['custom message'], @person.errors[:title] - end - - def test_validates_length_of_is_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:wrong_length => 'global message'}} - - Person.validates_length_of :title, :is => 5 - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_length_of", :wrong_length do |person, options_to_merge| + Person.validates_length_of :title, options_to_merge.merge(:is => 5) end # validates_format_of w/o mocha - def test_validates_format_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:invalid => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:invalid => 'global message'}} - - Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ - @person.valid? - assert_equal ['custom message'], @person.errors[:title] - end - - def test_validates_format_of_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:invalid => 'global message'}} - - Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_format_of", :invalid do |person, options_to_merge| + Person.validates_format_of :title, options_to_merge.merge(:with => /^[1-9][0-9]*$/) end # validates_inclusion_of w/o mocha - def test_validates_inclusion_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:inclusion => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:inclusion => 'global message'}} - - Person.validates_inclusion_of :title, :in => %w(a b c) - @person.valid? - assert_equal ['custom message'], @person.errors[:title] - end - - def test_validates_inclusion_of_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:inclusion => 'global message'}} - - Person.validates_inclusion_of :title, :in => %w(a b c) - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_inclusion_of", :inclusion do |person, options_to_merge| + Person.validates_inclusion_of :title, options_to_merge.merge(:in => %w(a b c)) end # validates_exclusion_of w/o mocha - def test_validates_exclusion_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:exclusion => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:exclusion => 'global message'}} - - Person.validates_exclusion_of :title, :in => %w(a b c) - @person.title = 'a' - @person.valid? - assert_equal ['custom message'], @person.errors[:title] - end - - def test_validates_exclusion_of_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:exclusion => 'global message'}} - - Person.validates_exclusion_of :title, :in => %w(a b c) - @person.title = 'a' - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_exclusion_of", :exclusion do |person, options_to_merge| + Person.validates_exclusion_of :title, options_to_merge.merge(:in => %w(a b c)) + person.title = 'a' end # validates_numericality_of without :only_integer w/o mocha - def test_validates_numericality_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}} - - Person.validates_numericality_of :title - @person.title = 'a' - @person.valid? - assert_equal ['custom message'], @person.errors[:title] - end - - def test_validates_numericality_of_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}} - - Person.validates_numericality_of :title, :only_integer => true - @person.title = 'a' - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_numericality_of", :not_a_number do |person, options_to_merge| + Person.validates_numericality_of :title, options_to_merge + person.title = 'a' end # validates_numericality_of with :only_integer w/o mocha - def test_validates_numericality_of_only_integer_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:not_an_integer => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:not_an_integer => 'global message'}} - - Person.validates_numericality_of :title, :only_integer => true - @person.title = '1.0' - @person.valid? - assert_equal ['custom message'], @person.errors[:title] - end - - def test_validates_numericality_of_only_integer_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:not_an_integer => 'global message'}} - - Person.validates_numericality_of :title, :only_integer => true - @person.title = '1.0' - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_numericality_of", :not_an_integer do |person, options_to_merge| + Person.validates_numericality_of :title, options_to_merge.merge(:only_integer => true) + person.title = '1.0' end # validates_numericality_of :odd w/o mocha - def test_validates_numericality_of_odd_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:odd => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:odd => 'global message'}} - - Person.validates_numericality_of :title, :only_integer => true, :odd => true - @person.title = 0 - @person.valid? - assert_equal ['custom message'], @person.errors[:title] - end - - def test_validates_numericality_of_odd_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:odd => 'global message'}} - - Person.validates_numericality_of :title, :only_integer => true, :odd => true - @person.title = 0 - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_numericality_of", :odd do |person, options_to_merge| + Person.validates_numericality_of :title, options_to_merge.merge(:only_integer => true, :odd => true) + person.title = 0 end # validates_numericality_of :less_than w/o mocha - def test_validates_numericality_of_less_than_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:less_than => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:less_than => 'global message'}} - - Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 - @person.title = 1 - @person.valid? - assert_equal ['custom message'], @person.errors[:title] - end - - def test_validates_numericality_of_less_than_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:less_than => 'global message'}} - - Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 - @person.title = 1 - @person.valid? - assert_equal ['global message'], @person.errors[:title] + set_expectations_for_validation "validates_numericality_of", :less_than do |person, options_to_merge| + Person.validates_numericality_of :title, options_to_merge.merge(:only_integer => true, :less_than => 0) + person.title = 1 end # test with validates_with diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index 8e77a0222e..e1d7d40c47 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -4,6 +4,8 @@ require 'cases/helper' require 'models/topic' require 'models/person' +require 'bigdecimal' + class NumericalityValidationTest < ActiveModel::TestCase def teardown diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb index b1450586a8..510c13a7c3 100644 --- a/activemodel/test/cases/validations/presence_validation_test.rb +++ b/activemodel/test/cases/validations/presence_validation_test.rb @@ -32,7 +32,7 @@ class PresenceValidationTest < ActiveModel::TestCase assert t.valid? end - test 'accepts array arguments' do + def test_accepts_array_arguments Topic.validates_presence_of %w(title content) t = Topic.new assert t.invalid? diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index af195af080..e94d8ce88c 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -6,6 +6,9 @@ require 'models/reply' require 'models/custom_reader' require 'models/automobile' +require 'active_support/json' +require 'active_support/xml_mini' + class ValidationsTest < ActiveModel::TestCase def setup @@ -83,10 +86,9 @@ class ValidationsTest < ActiveModel::TestCase r = Reply.new r.content = "Mismatch" r.valid? - r.errors[:base] << "Reply is not dignifying" + r.errors.add(:base, "Reply is not dignifying") - errors = [] - r.errors.to_a.each { |error| errors << error } + errors = r.errors.to_a.inject([]) { |result, error| result + [error] } assert_equal ["Reply is not dignifying"], r.errors[:base] @@ -95,6 +97,22 @@ class ValidationsTest < ActiveModel::TestCase assert_equal 2, r.errors.count end + def test_errors_on_base_with_symbol_message + r = Reply.new + r.content = "Mismatch" + r.valid? + r.errors.add(:base, :invalid) + + errors = r.errors.to_a.inject([]) { |result, error| result + [error] } + + assert_equal ["is invalid"], r.errors[:base] + + assert errors.include?("Title is Empty") + assert errors.include?("is invalid") + + assert_equal 2, r.errors.count + end + def test_errors_empty_after_errors_on_check t = Topic.new assert t.errors[:id].empty? @@ -143,12 +161,18 @@ class ValidationsTest < ActiveModel::TestCase end end - def test_errors_to_xml - r = Reply.new :title => "Wrong Create" - assert r.invalid? - xml = r.errors.to_xml(:skip_instruct => true) - assert_equal "<errors>", xml.first(8) - assert xml.include?("<error>Content is Empty</error>") + def test_errors_conversions + Topic.validates_presence_of %w(title content) + t = Topic.new + assert t.invalid? + + xml = t.errors.to_xml + assert_match %r{<errors>}, xml + assert_match %r{<error>Title can't be blank</error>}, xml + assert_match %r{<error>Content can't be blank</error>}, xml + + json = t.errors.to_json + assert_equal t.errors.to_a.to_json, json end def test_validation_order diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index def519c05d..a1a82fdff5 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,6 +1,12 @@ *Rails 3.0.0 [RC1] (unreleased)* -* PostgreSQL: ensure the database time zone matches Ruby's time zone. #4895 [Aaron Patterson] +* Add scoping and unscoped as the syntax to replace the old with_scope and with_exclusive_scope [José Valim] + +* New rake task, db:migrate:status, displays status of migrations #4947 [Kevin Skoglund] + +* select and order for ActiveRecord now always concatenate nested calls. Use reorder if you want the original order to be overwritten [Santiago Pastorino] + +* PostgreSQL: ensure the database time zone matches Ruby's time zone #4895 [Aaron Patterson] *Rails 3.0.0 [beta 4] (June 8th, 2010)* @@ -309,7 +315,7 @@ * Add first/last methods to associations/named_scope. Resolved #226. [Ryan Bates] -* Added SQL escaping for :limit and :offset #288 [Aaron Bedra, Steven Bristol, Jonathan Wiess] +* Added SQL escaping for :limit and :offset #288 [Aaron Bedra, Steven Bristol, Jonathan Wiess] * Added first/last methods to associations/named_scope. Resolved #226. [Ryan Bates] @@ -620,7 +626,7 @@ so newlines etc are escaped #10385 [Norbert Crombach] * Ensure that mysql quotes table names with database names correctly. Closes #9911 [crayz] - "foo.bar" => "`foo`.`bar`" + "foo.bar" => "`foo`.`bar`" * Complete the assimilation of Sexy Migrations from ErrFree [Chris Wanstrath, PJ Hyett] http://errtheblog.com/post/2381 @@ -755,7 +761,7 @@ single-table inheritance. #3833, #9886 [Gabriel Gironda, rramdas, François Bea # Ensure that has_many :through associations use a count query instead of loading the target when #size is called. Closes #8800 [Pratik Naik] -* Added :unless clause to validations #8003 [monki]. Example: +* Added :unless clause to validations #8003 [monki]. Example: def using_open_id? !identity_url.blank? @@ -775,7 +781,7 @@ single-table inheritance. #3833, #9886 [Gabriel Gironda, rramdas, François Bea OLD belongs_to :visitor, :class_name => 'User' # => inferred foreign_key is user_id - + NEW belongs_to :visitor, :class_name => 'User' # => inferred foreign_key is visitor_id @@ -881,9 +887,9 @@ single-table inheritance. #3833, #9886 [Gabriel Gironda, rramdas, François Bea t.column "created_at", :datetime t.column "updated_at", :datetime end - + ...can now be written as: - + create_table :products do |t| t.integer :shop_id, :creator_id t.string :name, :value, :default => "Untitled" @@ -959,7 +965,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Reworked David's query cache to be available as Model.cache {...}. For the duration of the block no select query should be run more then once. Any inserts/deletes/executes will flush the whole cache however [Tobias Lütke] Task.cache { Task.find(1); Task.find(1) } #=> 1 query - + * When dealing with SQLite3, use the table_info pragma helper, so that the bindings can do some translation for when sqlite3 breaks incompatibly between point releases. [Jamis Buck] * Oracle: fix lob and text default handling. #7344 [gfriedrich, Michael Schoen] @@ -1228,7 +1234,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Cache nil results for has_one associations so multiple calls don't call the database. Closes #5757. [Michael Schoen] -* Add documentation for how to disable timestamps on a per model basis. Closes #5684. [matt@mattmargolis.net Marcel Molina Jr.] +* Add documentation for how to disable timestamps on a per model basis. Closes #5684. [matt@mattmargolis.net Marcel Molina Jr.] * Don't save has_one associations unnecessarily. #5735 [Jonathan Viney] @@ -1248,8 +1254,8 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Added support for conditions on Base.exists? #5689 [Josh Peek]. Examples: - assert (Topic.exists?(:author_name => "David")) - assert (Topic.exists?(:author_name => "Mary", :approved => true)) + assert (Topic.exists?(:author_name => "David")) + assert (Topic.exists?(:author_name => "Mary", :approved => true)) assert (Topic.exists?(["parent_id = ?", 1])) * Schema dumper quotes date :default values. [Dave Thomas] @@ -1327,14 +1333,14 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Added simple hash conditions to find that'll just convert hash to an AND-based condition string #5143 [Hampton Catlin]. Example: - Person.find(:all, :conditions => { :last_name => "Catlin", :status => 1 }, :limit => 2) + Person.find(:all, :conditions => { :last_name => "Catlin", :status => 1 }, :limit => 2) ...is the same as: Person.find(:all, :conditions => [ "last_name = ? and status = ?", "Catlin", 1 ], :limit => 2) - + This makes it easier to pass in the options from a form or otherwise outside. - + * Fixed issues with BLOB limits, charsets, and booleans for Firebird #5194, #5191, #5189 [kennethkunz@gmail.com] @@ -1362,7 +1368,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Fix the has_and_belongs_to_many #create doesn't populate the join for new records. Closes #3692 [Josh Susser] -* Provide Association Extensions access to the instance that the association is being accessed from. +* Provide Association Extensions access to the instance that the association is being accessed from. Closes #4433 [Josh Susser] * Update OpenBase adaterp's maintainer's email address. Closes #5176. [Derrick Spell] @@ -1381,7 +1387,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Call Inflector#demodulize on the class name when eagerly including an STI model. Closes #5077 [info@loobmedia.com] -* Preserve MySQL boolean column defaults when changing a column in a migration. Closes #5015. [pdcawley@bofh.org.uk] +* Preserve MySQL boolean column defaults when changing a column in a migration. Closes #5015. [pdcawley@bofh.org.uk] * PostgreSQL: migrations support :limit with :integer columns by mapping limit < 4 to smallint, > 4 to bigint, and anything else to integer. #2900 [keegan@thebasement.org] @@ -1391,7 +1397,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Ensure that StringIO is always available for the Schema dumper. [Marcel Molina Jr.] -* Allow AR::Base#to_xml to include methods too. Closes #4921. [johan@textdrive.com] +* Allow AR::Base#to_xml to include methods too. Closes #4921. [johan@textdrive.com] * Replace superfluous name_to_class_name variant with camelize. [Marcel Molina Jr.] @@ -1401,7 +1407,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Remove duplicate fixture entry in comments.yml. Closes #4923. [Blair Zajac <blair@orcaware.com>] -* Update FrontBase adapter to check binding version. Closes #4920. [mlaster@metavillage.com] +* Update FrontBase adapter to check binding version. Closes #4920. [mlaster@metavillage.com] * New Frontbase connections don't start in auto-commit mode. Closes #4922. [mlaster@metavillage.com] @@ -1413,9 +1419,9 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Add warning about the proper way to validate the presence of a foreign key. Closes #4147. [Francois Beausoleil <francois.beausoleil@gmail.com>] -* Fix syntax error in documentation. Closes #4679. [Mislav Marohnić] +* Fix syntax error in documentation. Closes #4679. [Mislav Marohnić] -* Add Oracle support for CLOB inserts. Closes #4748. [schoenm@earthlink.net sandra.metz@duke.edu] +* Add Oracle support for CLOB inserts. Closes #4748. [schoenm@earthlink.net sandra.metz@duke.edu] * Various fixes for sqlserver_adapter (odbc statement finishing, ado schema dumper, drop index). Closes #4831. [kajism@yahoo.com] @@ -1466,7 +1472,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] *1.15.3* (March 12th, 2007) - * Allow a polymorphic :source for has_many :through associations. Closes #7143 [protocool] + * Allow a polymorphic :source for has_many :through associations. Closes #7143 [protocool] * Consistently quote primary key column names. #7763 [toolmantim] @@ -1705,8 +1711,8 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Added support for conditions on Base.exists? #5689 [Josh Peek]. Examples: - assert (Topic.exists?(:author_name => "David")) - assert (Topic.exists?(:author_name => "Mary", :approved => true)) + assert (Topic.exists?(:author_name => "David")) + assert (Topic.exists?(:author_name => "Mary", :approved => true)) assert (Topic.exists?(["parent_id = ?", 1])) * Schema dumper quotes date :default values. [Dave Thomas] @@ -1780,7 +1786,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Added simple hash conditions to find that'll just convert hash to an AND-based condition string #5143 [Hampton Catlin]. Example: - Person.find(:all, :conditions => { :last_name => "Catlin", :status => 1 }, :limit => 2) + Person.find(:all, :conditions => { :last_name => "Catlin", :status => 1 }, :limit => 2) ...is the same as: @@ -1815,7 +1821,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Fix the has_and_belongs_to_many #create doesn't populate the join for new records. Closes #3692 [Josh Susser] -* Provide Association Extensions access to the instance that the association is being accessed from. +* Provide Association Extensions access to the instance that the association is being accessed from. Closes #4433 [Josh Susser] * Update OpenBase adaterp's maintainer's email address. Closes #5176. [Derrick Spell] @@ -1834,7 +1840,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Call Inflector#demodulize on the class name when eagerly including an STI model. Closes #5077 [info@loobmedia.com] -* Preserve MySQL boolean column defaults when changing a column in a migration. Closes #5015. [pdcawley@bofh.org.uk] +* Preserve MySQL boolean column defaults when changing a column in a migration. Closes #5015. [pdcawley@bofh.org.uk] * PostgreSQL: migrations support :limit with :integer columns by mapping limit < 4 to smallint, > 4 to bigint, and anything else to integer. #2900 [keegan@thebasement.org] @@ -1844,7 +1850,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Ensure that StringIO is always available for the Schema dumper. [Marcel Molina Jr.] -* Allow AR::Base#to_xml to include methods too. Closes #4921. [johan@textdrive.com] +* Allow AR::Base#to_xml to include methods too. Closes #4921. [johan@textdrive.com] * Remove duplicate fixture entry in comments.yml. Closes #4923. [Blair Zajac <blair@orcaware.com>] @@ -1854,9 +1860,9 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Add warning about the proper way to validate the presence of a foreign key. Closes #4147. [Francois Beausoleil <francois.beausoleil@gmail.com>] -* Fix syntax error in documentation. Closes #4679. [Mislav Marohnić] +* Fix syntax error in documentation. Closes #4679. [Mislav Marohnić] -* Add Oracle support for CLOB inserts. Closes #4748. [schoenm@earthlink.net sandra.metz@duke.edu] +* Add Oracle support for CLOB inserts. Closes #4748. [schoenm@earthlink.net sandra.metz@duke.edu] * Various fixes for sqlserver_adapter (odbc statement finishing, ado schema dumper, drop index). Closes #4831. [kajism@yahoo.com] @@ -1904,7 +1910,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Add warning about the proper way to validate the presence of a foreign key. #4147 [Francois Beausoleil <francois.beausoleil@gmail.com>] -* Fix syntax error in documentation. #4679 [Mislav Marohnić] +* Fix syntax error in documentation. #4679 [Mislav Marohnić] * Update inconsistent migrations documentation. #4683 [machomagna@gmail.com] @@ -2200,7 +2206,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] <id type="integer">1</id> <credit-limit type="integer">50</credit-limit> </account> - </firm> + </firm> * Allow :counter_cache to take a column name for custom counter cache columns [Jamis Buck] @@ -2390,17 +2396,17 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Fixed that saving a model with multiple habtm associations would only save the first one. #3244 [yanowitz-rubyonrails@quantumfoam.org, Florian Weber] -* Fix change_column to work with PostgreSQL 7.x and 8.x. #3141 [wejn@box.cz, Rick Olson, Scott Barron] +* Fix change_column to work with PostgreSQL 7.x and 8.x. #3141 [wejn@box.cz, Rick Olson, Scott Barron] -* removed :piggyback in favor of just allowing :select on :through associations. [Tobias Lütke] +* removed :piggyback in favor of just allowing :select on :through associations. [Tobias Lütke] -* made method missing delegation to class methods on relation target work on :through associations. [Tobias Lütke] +* made method missing delegation to class methods on relation target work on :through associations. [Tobias Lütke] -* made .find() work on :through relations. [Tobias Lütke] +* made .find() work on :through relations. [Tobias Lütke] * Fix typo in association docs. #3296. [Blair Zajac] -* Fixed :through relations when using STI inherited classes would use the inherited class's name as foreign key on the join model [Tobias Lütke] +* Fixed :through relations when using STI inherited classes would use the inherited class's name as foreign key on the join model [Tobias Lütke] *1.13.2* (December 13th, 2005) @@ -2663,7 +2669,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * YAML fixtures support ordered hashes for fixtures with foreign key dependencies in the same table. #1896 [purestorm@ggnore.net] -* :dependent now accepts :nullify option. Sets the foreign key of the related objects to NULL instead of deleting them. #2015 [Robby Russell <robby@planetargon.com>] +* :dependent now accepts :nullify option. Sets the foreign key of the related objects to NULL instead of deleting them. #2015 [Robby Russell <robby@planetargon.com>] * Introduce read-only records. If you call object.readonly! then it will mark the object as read-only and raise ReadOnlyRecord if you call object.save. object.readonly? reports whether the object is read-only. Passing :readonly => true to any finder method will mark returned records as read-only. The :joins option now implies :readonly, so if you use this option, saving the same record will now fail. Use find_by_sql to work around. @@ -2818,7 +2824,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio def self.search(q) find(:all, :conditions => ["body = ?", q]) end - end + end class Post < AR:B has_many :comments @@ -2877,7 +2883,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio def evaluate_velocity(developer) ... end - end + end ..raising an exception will cause the object not to be added (or removed, with before_remove). @@ -2913,10 +2919,10 @@ in effect. Added :readonly finder constraint. Calling an association collectio ...should instead be: Developer.find( - :all, - :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :all, + :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', :conditions => 'project_id=1' - ) + ) * Fixed that validations didn't respecting custom setting for too_short, too_long messages #1437 [Marcel Molina Jr.] @@ -2953,7 +2959,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Return PostgreSQL columns in the order they were declared #1374 [perlguy@gmail.com] -* Allow before/after update hooks to work on models using optimistic locking +* Allow before/after update hooks to work on models using optimistic locking * Eager loading of dependent has_one associations won't delete the association #1212 @@ -3133,10 +3139,10 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added adapter independent limit clause as a two-element array with the first being the limit, the second being the offset #795 [Sam Stephenson]. Example: - Developer.find_all nil, 'id ASC', 5 # return the first five developers + Developer.find_all nil, 'id ASC', 5 # return the first five developers Developer.find_all nil, 'id ASC', [3, 8] # return three developers, starting from #8 and forward - This doesn't yet work with the DB2 or MS SQL adapters. Patches to make that happen are encouraged. + This doesn't yet work with the DB2 or MS SQL adapters. Patches to make that happen are encouraged. * Added alias_method :to_param, :id to Base, such that Active Record objects to be used as URL parameters in Action Pack automatically #812 [Nicholas Seckar/Sam Stephenson] @@ -3171,8 +3177,8 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added MultiparameterAssignmentErrors and AttributeAssignmentError exceptions #777 [demetrius]. Documentation: - * +MultiparameterAssignmentErrors+ -- collection of errors that occurred during a mass assignment using the - +attributes=+ method. The +errors+ property of this exception contains an array of +AttributeAssignmentError+ + * +MultiparameterAssignmentErrors+ -- collection of errors that occurred during a mass assignment using the + +attributes=+ method. The +errors+ property of this exception contains an array of +AttributeAssignmentError+ objects that should be inspected to determine which attributes triggered the errors. * +AttributeAssignmentError+ -- an error occurred while doing a mass assignment through the +attributes=+ method. You can inspect the +attribute+ property of the exception object to determine which attribute triggered the error. @@ -3258,11 +3264,11 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed the SQL Server adapter on a bunch of issues #667 [DeLynn] - 1. Created a new columns method that is much cleaner. - 2. Corrected a problem with the select and select_all methods - that didn't account for the LIMIT clause being passed into raw SQL statements. - 3. Implemented the string_to_time method in order to create proper instances of the time class. - 4. Added logic to the simplified_type method that allows the database to specify the scale of float data. + 1. Created a new columns method that is much cleaner. + 2. Corrected a problem with the select and select_all methods + that didn't account for the LIMIT clause being passed into raw SQL statements. + 3. Implemented the string_to_time method in order to create proper instances of the time class. + 4. Added logic to the simplified_type method that allows the database to specify the scale of float data. 5. Adjusted the quote_column_name to account for the fact that MS SQL is bothered by a forward slash in the data string. * Fixed that the dynamic finder like find_all_by_something_boolean(false) didn't work #649 [lmarlow] @@ -3385,9 +3391,9 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed binary support for PostgreSQL #444 [alex@byzantine.no] -* Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the - collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and calling collection.size if it has. If - it's more likely than not that the collection does have a size larger than zero and you need to fetch that collection afterwards, +* Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the + collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and calling collection.size if it has. If + it's more likely than not that the collection does have a size larger than zero and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length. * Added Base#attributes that returns a hash of all the attributes with their names as keys and clones of their objects as values #433 [atyp.de] @@ -3400,7 +3406,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added SQLite3 compatibility through the sqlite3-ruby adapter by Jamis Buck #381 [Jeremy Kemper] -* Added support for the new protocol spoken by MySQL 4.1.1+ servers for the Ruby/MySQL adapter that ships with Rails #440 [Matt Mower] +* Added support for the new protocol spoken by MySQL 4.1.1+ servers for the Ruby/MySQL adapter that ships with Rails #440 [Matt Mower] * Added that Observers can use the observes class method instead of overwriting self.observed_class(). @@ -3463,7 +3469,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio class Post < ActiveRecord::Base has_one :tagging, :as => :taggable end - + Post.find :all, :include => :tagging * Added descriptive error messages for invalid has_many :through associations: going through :has_one or :has_and_belongs_to_many [Rick Olson] @@ -3474,7 +3480,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio has_many :photos, :as => :photographic belongs_to :firm end - + class Firm < ActiveRecord::Base has_many :photo_collections has_many :photos, :through => :photo_collections @@ -3536,7 +3542,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio In this example, :include => false disables the default eager association from loading. :select changes the standard select clause. :joins specifies a join that is added to the end of the has_many :through query. - + class Post < ActiveRecord::Base has_many :tags, :through => :taggings, :include => :tagging do def add_joins_and_select @@ -3545,7 +3551,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio end end end - + * Fixed that schema changes while the database was open would break any connections to a SQLite database (now we reconnect if that error is throw) [David Heinemeier Hansson] * Don't classify the has_one class when eager loading, it is already singular. Add tests. (closes #4117) [Jonathan Viney] @@ -3562,12 +3568,12 @@ in effect. Added :readonly finder constraint. Calling an association collectio has_many :clients has_many :invoices, :through => :clients end - + class Client < ActiveRecord::Base belongs_to :firm has_many :invoices end - + class Invoice < ActiveRecord::Base belongs_to :client end @@ -3619,19 +3625,19 @@ in effect. Added :readonly finder constraint. Calling an association collectio class CachedModel < ActiveRecord::Base self.abstract_class = true end - + class Post < CachedModel end - + CachedModel.abstract_class? => true - + Post.abstract_class? => false Post.base_class => Post - + Post.table_name => 'posts' @@ -3656,9 +3662,9 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added Base#to_xml that'll turn the current record into a XML representation [David Heinemeier Hansson]. Example: topic.to_xml - + ...returns: - + <?xml version="1.0" encoding="UTF-8"?> <topic> <title>The First Topic</title> @@ -3673,13 +3679,13 @@ in effect. Added :readonly finder constraint. Calling an association collectio <parent-id></parent-id> <last-read type="date">2004-04-15</last-read> </topic> - + ...and you can configure with: - + topic.to_xml(:skip_instruct => true, :except => [ :id, bonus_time, :written_on, replies_count ]) - + ...that'll return: - + <topic> <title>The First Topic</title> <author-name>David</author-name> @@ -3689,13 +3695,13 @@ in effect. Added :readonly finder constraint. Calling an association collectio <parent-id></parent-id> <last-read type="date">2004-04-15</last-read> </topic> - + You can even do load first-level associations as part of the document: - + firm.to_xml :include => [ :account, :clients ] - + ...that'll return something like: - + <?xml version="1.0" encoding="UTF-8"?> <firm> <id type="integer">1</id> @@ -3715,7 +3721,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio <id type="integer">1</id> <credit-limit type="integer">50</credit-limit> </account> - </firm> + </firm> * Allow :counter_cache to take a column name for custom counter cache columns [Jamis Buck] @@ -3733,9 +3739,9 @@ in effect. Added :readonly finder constraint. Calling an association collectio * The first time a table is referenced in a join, no alias is used. * After that, the parent class name and the reflection name are used. - + Tree.find(:all, :include => :children) # LEFT OUTER JOIN trees AS tree_children ... - + * Any additional join references get a numerical suffix like '_2', '_3', etc. * Fixed eager loading problems with single-table inheritance #3580 [Rick Olson]. Post.find(:all, :include => :special_comments) now returns all posts, and any special comments that the posts may have. And made STI work with has_many :through and polymorphic belongs_to. @@ -3747,28 +3753,28 @@ in effect. Added :readonly finder constraint. Calling an association collectio => authors +- posts +- comments - + # cascaded in two levels and normal association >> Author.find(:all, :include=>[{:posts=>:comments}, :categorizations]) => authors +- posts +- comments +- categorizations - + # cascaded in two levels with two has_many associations >> Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}) => authors +- posts +- comments +- categorizations - + # cascaded in three levels >> Company.find(:all, :include=>{:groups=>{:members=>{:favorites}}}) => companies +- groups +- members +- favorites - + * Make counter cache work when replacing an association #3245 [eugenol@gmail.com] * Make migrations verbose [Jamis Buck] @@ -3905,17 +3911,17 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed that saving a model with multiple habtm associations would only save the first one. #3244 [yanowitz-rubyonrails@quantumfoam.org, Florian Weber] -* Fix change_column to work with PostgreSQL 7.x and 8.x. #3141 [wejn@box.cz, Rick Olson, Scott Barron] +* Fix change_column to work with PostgreSQL 7.x and 8.x. #3141 [wejn@box.cz, Rick Olson, Scott Barron] -* removed :piggyback in favor of just allowing :select on :through associations. [Tobias Lütke] +* removed :piggyback in favor of just allowing :select on :through associations. [Tobias Lütke] -* made method missing delegation to class methods on relation target work on :through associations. [Tobias Lütke] +* made method missing delegation to class methods on relation target work on :through associations. [Tobias Lütke] -* made .find() work on :through relations. [Tobias Lütke] +* made .find() work on :through relations. [Tobias Lütke] * Fix typo in association docs. #3296. [Blair Zajac] -* Fixed :through relations when using STI inherited classes would use the inherited class's name as foreign key on the join model [Tobias Lütke] +* Fixed :through relations when using STI inherited classes would use the inherited class's name as foreign key on the join model [Tobias Lütke] *1.13.2* (December 13th, 2005) @@ -3928,7 +3934,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio class Post has_many :recent_comments, :class_name => "Comment", :limit => 10, :include => :author end - + post.recent_comments.find(:all) # Uses LIMIT 10 and includes authors post.recent_comments.find(:all, :limit => nil) # Uses no limit but include authors post.recent_comments.find(:all, :limit => nil, :include => nil) # Uses no limit and doesn't include authors @@ -4077,9 +4083,9 @@ in effect. Added :readonly finder constraint. Calling an association collectio # Associated with :post_id Comment.create :body => "Hello world" end - + This is rarely used directly, but allows for find_or_create on associations. So you can do: - + # If the tag doesn't exist, a new one is created that's associated with the person person.tags.find_or_create_by_name("Summer") @@ -4087,7 +4093,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio # No 'Summer' tag exists Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer") - + # Now the 'Summer' tag does exist Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer") @@ -4178,7 +4184,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * YAML fixtures support ordered hashes for fixtures with foreign key dependencies in the same table. #1896 [purestorm@ggnore.net] -* :dependent now accepts :nullify option. Sets the foreign key of the related objects to NULL instead of deleting them. #2015 [Robby Russell <robby@planetargon.com>] +* :dependent now accepts :nullify option. Sets the foreign key of the related objects to NULL instead of deleting them. #2015 [Robby Russell <robby@planetargon.com>] * Introduce read-only records. If you call object.readonly! then it will mark the object as read-only and raise ReadOnlyRecord if you call object.save. object.readonly? reports whether the object is read-only. Passing :readonly => true to any finder method will mark returned records as read-only. The :joins option now implies :readonly, so if you use this option, saving the same record will now fail. Use find_by_sql to work around. @@ -4333,14 +4339,14 @@ in effect. Added :readonly finder constraint. Calling an association collectio def self.search(q) find(:all, :conditions => ["body = ?", q]) end - end + end class Post < AR:B has_many :comments end Post.find(1).comments.search('hi') # => SELECT * from comments WHERE post_id = 1 AND body = 'hi' - + NOTICE: This patch changes the underlying SQL generated by has_and_belongs_to_many queries. If your relying on that, such as by explicitly referencing the old t and j aliases, you'll need to update your code. Of course, you _shouldn't_ be relying on details like that no less than you should be diving in to touch private variables. But just in case you do, consider yourself @@ -4388,14 +4394,14 @@ in effect. Added :readonly finder constraint. Calling an association collectio class Project has_and_belongs_to_many :developers, :before_add => :evaluate_velocity - + def evaluate_velocity(developer) ... end - end - + end + ..raising an exception will cause the object not to be added (or removed, with before_remove). - + * Fixed Base.content_columns call for SQL Server adapter #1450 [DeLynn Berry] @@ -4424,14 +4430,14 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed Base#find to honor the documentation on how :joins work and make them consistent with Base#count #1405 [pritchie@gmail.com]. What used to be: Developer.find :all, :joins => 'developers_projects', :conditions => 'id=developer_id AND project_id=1' - + ...should instead be: - + Developer.find( - :all, - :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :all, + :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', :conditions => 'project_id=1' - ) + ) * Fixed that validations didn't respecting custom setting for too_short, too_long messages #1437 [Marcel Molina Jr.] @@ -4450,12 +4456,12 @@ in effect. Added :readonly finder constraint. Calling an association collectio david.projects = [Project.find(1), Project.new("name" => "ActionWebSearch")] david.save - + If david.projects already contain the project with ID 1, this is left unchanged. Any other projects are dropped. And the new project is saved when david.save is called. - + Also included is a way to do assignments through IDs, which is perfect for checkbox updating, so you get to do: - + david.project_ids = [1, 5, 7] * Corrected typo in find SQL for has_and_belongs_to_many. #1312 [ben@bensinclair.com] @@ -4468,7 +4474,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Return PostgreSQL columns in the order they were declared #1374 [perlguy@gmail.com] -* Allow before/after update hooks to work on models using optimistic locking +* Allow before/after update hooks to work on models using optimistic locking * Eager loading of dependent has_one associations won't delete the association #1212 @@ -4499,7 +4505,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio Conditional validations can also solve the salted login generator problem: validates_confirmation_of :password, :if => :new_password? - + Using blocks: validates_presence_of :username, :if => Proc.new { |user| user.signup_step > 1 } @@ -4562,11 +4568,11 @@ in effect. Added :readonly finder constraint. Calling an association collectio puts "Written by: " + post.author.name puts "Last comment on: " + post.comments.first.created_on end - + This used to generate 301 database queries if all 100 posts had both author and comments. It can now be written as: - + for post in Post.find(:all, :limit => 100, :include => [ :author, :comments ]) - + ...and the number of database queries needed is now 1. * Added new unified Base.find API and deprecated the use of find_first and find_all. See the documentation for Base.find. Examples: @@ -4648,10 +4654,10 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added adapter independent limit clause as a two-element array with the first being the limit, the second being the offset #795 [Sam Stephenson]. Example: - Developer.find_all nil, 'id ASC', 5 # return the first five developers + Developer.find_all nil, 'id ASC', 5 # return the first five developers Developer.find_all nil, 'id ASC', [3, 8] # return three developers, starting from #8 and forward - - This doesn't yet work with the DB2 or MS SQL adapters. Patches to make that happen are encouraged. + + This doesn't yet work with the DB2 or MS SQL adapters. Patches to make that happen are encouraged. * Added alias_method :to_param, :id to Base, such that Active Record objects to be used as URL parameters in Action Pack automatically #812 [Nicholas Seckar/Sam Stephenson] @@ -4686,8 +4692,8 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added MultiparameterAssignmentErrors and AttributeAssignmentError exceptions #777 [demetrius]. Documentation: - * +MultiparameterAssignmentErrors+ -- collection of errors that occurred during a mass assignment using the - +attributes=+ method. The +errors+ property of this exception contains an array of +AttributeAssignmentError+ + * +MultiparameterAssignmentErrors+ -- collection of errors that occurred during a mass assignment using the + +attributes=+ method. The +errors+ property of this exception contains an array of +AttributeAssignmentError+ objects that should be inspected to determine which attributes triggered the errors. * +AttributeAssignmentError+ -- an error occurred while doing a mass assignment through the +attributes=+ method. You can inspect the +attribute+ property of the exception object to determine which attribute triggered the error. @@ -4715,16 +4721,16 @@ in effect. Added :readonly finder constraint. Calling an association collectio Validates whether the value of the specified attribute is numeric by trying to convert it to a float with Kernel.Float (if <tt>integer</tt> is false) or applying it to the regular expression <tt>/^[\+\-]?\d+$/</tt> (if <tt>integer</tt> is set to true). - + class Person < ActiveRecord::Base validates_numericality_of :value, :on => :create end - + Configuration options: * <tt>message</tt> - A custom error message (default is: "is not a number") * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update) * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false) - + * Fixed that HasManyAssociation#count was using :finder_sql rather than :counter_sql if it was available #445 [Scott Barron] @@ -4773,17 +4779,17 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed the SQL Server adapter on a bunch of issues #667 [DeLynn] - 1. Created a new columns method that is much cleaner. - 2. Corrected a problem with the select and select_all methods - that didn't account for the LIMIT clause being passed into raw SQL statements. - 3. Implemented the string_to_time method in order to create proper instances of the time class. - 4. Added logic to the simplified_type method that allows the database to specify the scale of float data. + 1. Created a new columns method that is much cleaner. + 2. Corrected a problem with the select and select_all methods + that didn't account for the LIMIT clause being passed into raw SQL statements. + 3. Implemented the string_to_time method in order to create proper instances of the time class. + 4. Added logic to the simplified_type method that allows the database to specify the scale of float data. 5. Adjusted the quote_column_name to account for the fact that MS SQL is bothered by a forward slash in the data string. * Fixed that the dynamic finder like find_all_by_something_boolean(false) didn't work #649 [lmarlow] * Added validates_each that validates each specified attribute against a block #610 [Jeremy Kemper]. Example: - + class Person < ActiveRecord::Base validates_each :first_name, :last_name do |record, attr| record.errors.add attr, 'starts with z.' if attr[0] == ?z @@ -4861,19 +4867,19 @@ in effect. Added :readonly finder constraint. Calling an association collectio class Book < ActiveRecord::Base has_many :pages belongs_to :library - + validates_associated :pages, :library end - + * Added support for associating unsaved objects #402 [Tim Bates]. Rules that govern this addition: == Unsaved objects and associations - + You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be aware of, mostly involving the saving of associated objects. - + === One-to-one associations - + * Assigning an object to a has_one association automatically saves that object, and the object being replaced (if there is one), in order to update their primary keys - except if the parent object is unsaved (new_record? == true). * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment @@ -4881,9 +4887,9 @@ in effect. Added :readonly finder constraint. Calling an association collectio * If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below). * Assigning an object to a belongs_to association does not save the object, since the foreign key field belongs on the parent. It does not save the parent either. - + === Collections - + * Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object (the owner of the collection) is not yet stored in the database. * If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false. @@ -4900,9 +4906,9 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed binary support for PostgreSQL #444 [alex@byzantine.no] -* Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the - collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and calling collection.size if it has. If - it's more likely than not that the collection does have a size larger than zero and you need to fetch that collection afterwards, +* Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the + collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and calling collection.size if it has. If + it's more likely than not that the collection does have a size larger than zero and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length. * Added Base#attributes that returns a hash of all the attributes with their names as keys and clones of their objects as values #433 [atyp.de] @@ -4915,7 +4921,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added SQLite3 compatibility through the sqlite3-ruby adapter by Jamis Buck #381 [Jeremy Kemper] -* Added support for the new protocol spoken by MySQL 4.1.1+ servers for the Ruby/MySQL adapter that ships with Rails #440 [Matt Mower] +* Added support for the new protocol spoken by MySQL 4.1.1+ servers for the Ruby/MySQL adapter that ships with Rails #440 [Matt Mower] * Added that Observers can use the observes class method instead of overwriting self.observed_class(). @@ -4923,7 +4929,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio class ListSweeper < ActiveRecord::Base def self.observed_class() [ List, Item ] end - + After: class ListSweeper < ActiveRecord::Base observes List, Item @@ -4943,7 +4949,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio page.views # => 1 page.increment!(:views) # executes an UPDATE statement page.views # => 2 - + page.increment(:views).increment!(:views) page.views # => 4 @@ -4955,32 +4961,32 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added automated optimistic locking if the field <tt>lock_version</tt> is present. Each update to the record increments the lock_version column and the locking facilities ensure that records instantiated twice will let the last one saved raise a StaleObjectError if the first was also updated. Example: - + p1 = Person.find(1) p2 = Person.find(1) - + p1.first_name = "Michael" p1.save - + p2.first_name = "should fail" p2.save # Raises a ActiveRecord::StaleObjectError - + You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, or otherwise apply the business logic needed to resolve the conflict. #384 [Michael Koziarski] -* Added dynamic attribute-based finders as a cleaner way of getting objects by simple queries without turning to SQL. +* Added dynamic attribute-based finders as a cleaner way of getting objects by simple queries without turning to SQL. They work by appending the name of an attribute to <tt>find_by_</tt>, so you get finders like <tt>Person.find_by_user_name, Payment.find_by_transaction_id</tt>. So instead of writing <tt>Person.find_first(["user_name = ?", user_name])</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>. - + It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like <tt>Person.find_by_user_name_and_password</tt> or even <tt>Payment.find_by_purchaser_and_state_and_country</tt>. So instead of writing - <tt>Person.find_first(["user_name = ? AND password = ?", user_name, password])</tt>, you just do + <tt>Person.find_first(["user_name = ? AND password = ?", user_name, password])</tt>, you just do <tt>Person.find_by_user_name_and_password(user_name, password)</tt>. - While primarily a construct for easier find_firsts, it can also be used as a construct for find_all by using calls like + While primarily a construct for easier find_firsts, it can also be used as a construct for find_all by using calls like <tt>Payment.find_all_by_amount(50)</tt> that is turned into <tt>Payment.find_all(["amount = ?", 50])</tt>. This is something not as equally useful, though, as it's not possible to specify the order in which the objects are returned. @@ -4988,13 +4994,13 @@ in effect. Added :readonly finder constraint. Calling an association collectio Before: before_destroy(Proc.new{ |record| Person.destroy_all "firm_id = #{record.id}" }) - + After: before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" } * Added :counter_cache option to acts_as_tree that works just like the one you can define on belongs_to #371 [Josh Peek] -* Added Base.default_timezone accessor that determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates +* Added Base.default_timezone accessor that determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database. This is set to :local by default. * Added the possibility for adapters to overwrite add_limit! to implement a different limiting scheme than "LIMIT X" used by MySQL, PostgreSQL, and SQLite. @@ -5013,7 +5019,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed a bug in the Ruby/MySQL that caused binary content to be escaped badly and come back mangled #405 [Tobias Lütke] -* Fixed that the const_missing autoload assumes the requested constant is set by require_association and calls const_get to retrieve it. +* Fixed that the const_missing autoload assumes the requested constant is set by require_association and calls const_get to retrieve it. If require_association did not set the constant then const_get will call const_missing, resulting in an infinite loop #380 [Jeremy Kemper] * Fixed broken transactions that were actually only running object-level and not db level transactions [andreas] @@ -5033,12 +5039,12 @@ in effect. Added :readonly finder constraint. Calling an association collectio require_association 'person' class Employee < Person end - + after: class Employee < Person end - This also reduces the usefulness of Controller.model in Action Pack to currently only being for documentation purposes. + This also reduces the usefulness of Controller.model in Action Pack to currently only being for documentation purposes. * Added that Base.update_all and Base.delete_all return an integer of the number of affected rows #341 @@ -5056,12 +5062,12 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added more informative exceptions in establish_connection #356 [Jeremy Kemper] -* Added Base#update_attributes that'll accept a hash of attributes and save the record (returning true if it passed validation, false otherwise). +* Added Base#update_attributes that'll accept a hash of attributes and save the record (returning true if it passed validation, false otherwise). Before: person.attributes = @params["person"] person.save - + Now: person.update_attributes(@params["person"]) @@ -5073,7 +5079,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed that options[:counter_sql] was overwritten with interpolated sql rather than original sql #355 [Jeremy Kemper] -* Fixed that overriding an attribute's accessor would be disregarded by add_on_empty and add_on_boundary_breaking because they simply used +* Fixed that overriding an attribute's accessor would be disregarded by add_on_empty and add_on_boundary_breaking because they simply used the attributes[] hash instead of checking for @base.respond_to?(attr.to_s). [Marten] * Fixed that Base.table_name would expect a parameter when used in has_and_belongs_to_many joins [Anna Lissa Cruz] @@ -5107,10 +5113,10 @@ in effect. Added :readonly finder constraint. Calling an association collectio belongs_to :todo_list end -* Added acts_as_tree that can decorates an existing class with a many to many relationship with itself. Perfect for categories in +* Added acts_as_tree that can decorates an existing class with a many to many relationship with itself. Perfect for categories in categories and the likes. [Tobias Lütke] -* Added that Active Records will automatically record creation and/or update timestamps of database objects if fields of the names +* Added that Active Records will automatically record creation and/or update timestamps of database objects if fields of the names created_at/created_on or updated_at/updated_on are present. [Tobias Lütke] * Added Base.default_error_messages as a hash of all the error messages used in the validates_*_of so they can be changed in one place [Tobias Lütke] @@ -5124,14 +5130,14 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added a better exception for when a type column is used in a table without the intention of triggering single-table inheritance. Example: ActiveRecord::SubclassNotFound: The single-table inheritance mechanism failed to locate the subclass: 'bad_class!'. - This error is raised because the column 'type' is reserved for storing the class in case of inheritance. - Please rename this column if you didn't intend it to be used for storing the inheritance class or + This error is raised because the column 'type' is reserved for storing the class in case of inheritance. + Please rename this column if you didn't intend it to be used for storing the inheritance class or overwrite Company.inheritance_column to use another column for that information. * Added that single-table inheritance will only kick in if the inheritance_column (by default "type") is present. Otherwise, inheritance won't have any magic side effects. -* Added the possibility of marking fields as being in error without adding a message (using nil) to it that'll get displayed wth full_messages #208 [mjobin] +* Added the possibility of marking fields as being in error without adding a message (using nil) to it that'll get displayed wth full_messages #208 [mjobin] * Fixed Base.errors to be indifferent as to whether strings or symbols are used. Examples: @@ -5145,7 +5151,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio errors.on(:name) # => "must be shorter" errors.on("name") # => "must be shorter" -* Added Base.validates_format_of that Validates whether the value of the specified attribute is of the correct form by matching +* Added Base.validates_format_of that Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression provided. [Marcel Molina Jr.] class Person < ActiveRecord::Base @@ -5155,14 +5161,14 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added Base.validates_length_of that delegates to add_on_boundary_breaking #312 [Tobias Lütke]. Example: Validates that the specified attribute matches the length restrictions supplied in either: - + - configuration[:minimum] - configuration[:maximum] - configuration[:is] - configuration[:within] (aka. configuration[:in]) - + Only one option can be used at a time. - + class Person < ActiveRecord::Base validates_length_of :first_name, :maximum=>30 validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind" @@ -5170,53 +5176,53 @@ in effect. Added :readonly finder constraint. Calling an association collectio validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character" validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me." end - + * Added Base.validate_presence as an alternative to implementing validate and doing errors.add_on_empty yourself. -* Added Base.validates_uniqueness_of that alidates whether the value of the specified attributes are unique across the system. +* Added Base.validates_uniqueness_of that alidates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user can be named "davidhh". - + class Person < ActiveRecord::Base validates_uniqueness_of :user_name end - + When the record is created, a check is performed to make sure that no record exist in the database with the given value for the specified attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. * Added Base.validates_confirmation_of that encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: - + Model: class Person < ActiveRecord::Base validates_confirmation_of :password end - + View: <%= password_field "person", "password" %> <%= password_field "person", "password_confirmation" %> - + The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual. It exists only as an in-memory variable for validating the password. This check is performed both on create and update. * Added Base.validates_acceptance_of that encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: - + class Person < ActiveRecord::Base validates_acceptance_of :terms_of_service end - + The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed both on create and update. NOTE: The agreement is considered valid if it's set to the string "1". This makes it easy to relate it to an HTML checkbox. - + * Added validation macros to make the stackable just like the lifecycle callbacks. Examples: class Person < ActiveRecord::Base validate { |record| record.errors.add("name", "too short") unless name.size > 10 } validate { |record| record.errors.add("name", "too long") unless name.size < 20 } validate_on_create :validate_password - + private def validate_password errors.add("password", "too short") unless password.size > 6 @@ -5232,7 +5238,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio through the database's own quoting routine. This should hopefully make it lots easier for new adapters that doesn't accept '1' for integer columns. -* Fixed has_and_belongs_to_many guessing of foreign key so that keys are generated correctly for models like SomeVerySpecialClient +* Fixed has_and_belongs_to_many guessing of foreign key so that keys are generated correctly for models like SomeVerySpecialClient [Florian Weber] * Added counter_sql option for has_many associations [Jeremy Kemper]. Documentation: @@ -5299,9 +5305,9 @@ in effect. Added :readonly finder constraint. Calling an association collectio fixtures/developers/fixtures.yaml fixtures/accounts/fixtures.yaml - + ...you now need to do: - + fixtures/developers.yaml fixtures/accounts.yaml @@ -5328,13 +5334,13 @@ in effect. Added :readonly finder constraint. Calling an association collectio name: David Heinemeier Hansson birthday: 1979-10-15 profession: Systems development - + steve: id: 2 name: Steve Ross Kellock birthday: 1974-09-27 profession: guy with keyboard - + The change is NOT backwards compatible. Fixtures written in the old YAML style needs to be rewritten! * All associations will now attempt to require the classes that they associate to. Relieving the need for most explicit 'require' statements. @@ -5342,7 +5348,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio *1.1.0* (34) -* Added automatic fixture setup and instance variable availability. Fixtures can also be automatically +* Added automatic fixture setup and instance variable availability. Fixtures can also be automatically instantiated in instance variables relating to their names using the following style: class FixturesTest < Test::Unit::TestCase @@ -5359,10 +5365,10 @@ in effect. Added :readonly finder constraint. Calling an association collectio attributes. This is really useful when you have information that's only relevant to the join itself, such as a "added_on" column for an association between post and category. The added attributes will automatically be injected into objects retrieved through the association similar to the piggy-back approach: - + post.categories.push_with_attributes(category, :added_on => Date.today) post.categories.first.added_on # => Date.today - + NOTE: The categories table doesn't have a added_on column, it's the categories_post join table that does! * Fixed that :exclusively_dependent and :dependent can't be activated at the same time on has_many associations [Jeremy Kemper] @@ -5415,8 +5421,8 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Improved the speed of respond_to? by placing the dynamic methods lookup table in a hash [geech] -* Added that any additional fields added to the join table in a has_and_belongs_to_many association - will be placed as attributes when pulling records out through has_and_belongs_to_many associations. +* Added that any additional fields added to the join table in a has_and_belongs_to_many association + will be placed as attributes when pulling records out through has_and_belongs_to_many associations. This is helpful when have information about the association itself that you want available on retrival. * Added better loading exception catching and RubyGems retries to the database adapters [alexeyv] @@ -5425,9 +5431,9 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed Base#transaction so that it returns the result of the last expression in the transaction block [alexeyv] -* Added Fixture#find to find the record corresponding to the fixture id. The record +* Added Fixture#find to find the record corresponding to the fixture id. The record class name is guessed by using Inflector#classify (also new) on the fixture directory name. - + Before: Document.find(@documents["first"]["id"]) After : @documents["first"].find @@ -5437,7 +5443,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed quoting problems on SQLite by adding quote_string to the AbstractAdapter that can be overwritten by the concrete adapters for a call to the dbm. [Andreas Schwarz] - + * Removed RubyGems backup strategy for requiring SQLite-adapter -- if people want to use gems, they're already doing it with AR. @@ -5453,19 +5459,19 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added serialize as a new class method to control when text attributes should be YAMLized or not. This means that automated serialization of hashes, arrays, and so on WILL NO LONGER HAPPEN (#10). You need to do something like this: - + class User < ActiveRecord::Base serialize :settings end - + This will assume that settings is a text column and will now YAMLize any object put in that attribute. You can also specify an optional :class_name option that'll raise an exception if a serialized object is retrieved as a descendant of a class not in the hierarchy. Example: - + class User < ActiveRecord::Base serialize :settings, :class_name => "Hash" end - + user = User.create("settings" => %w( one two three )) User.find(user.id).settings # => raises SerializationTypeMismatch @@ -5480,7 +5486,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added the possibility to chain the return of what happened inside a logged block [geech]: - This now works: + This now works: log { ... }.map { ... } Instead of doing: @@ -5490,7 +5496,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added "socket" option for the MySQL adapter, so you can change it to something else than "/tmp/mysql.sock" [Anna Lissa Cruz] -* Added respond_to? answers for all the attribute methods. So if Person has a name attribute retrieved from the table schema, +* Added respond_to? answers for all the attribute methods. So if Person has a name attribute retrieved from the table schema, person.respond_to? "name" will return true. * Added Base.benchmark which can be used to aggregate logging and benchmark, so you can measure and represent multiple statements in a single block. @@ -5511,7 +5517,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added compatibility with 2.x series of sqlite-ruby drivers. [Jamis Buck] * Added type safety for association assignments, so a ActiveRecord::AssociationTypeMismatch will be raised if you attempt to - assign an object that's not of the associated class. This cures the problem with nil giving id = 4 and fixnums giving id = 1 on + assign an object that's not of the associated class. This cures the problem with nil giving id = 4 and fixnums giving id = 1 on mistaken association assignments. [Reported by Andreas Schwarz] * Added the option to keep many fixtures in one single YAML document [what-a-day] @@ -5523,7 +5529,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added the option to observer more than one class at the time by specifying observed_class as an array -* Added auto-id propagation support for tables with arbitrary primary keys that have autogenerated sequences associated with them +* Added auto-id propagation support for tables with arbitrary primary keys that have autogenerated sequences associated with them on PostgreSQL. [Dave Steinberg] * Changed that integer and floats set to "" through attributes= remain as NULL. This was especially a problem for scaffolding and postgresql. (#49) @@ -5565,8 +5571,8 @@ in effect. Added :readonly finder constraint. Calling an association collectio directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used to guess the table name from even when called on Reply. The guessing rules are as follows: * Class name ends in "x", "ch" or "ss": "es" is appended, so a Search class becomes a searches table. - * Class name ends in "y" preceded by a consonant or "qu": The "y" is replaced with "ies", - so a Category class becomes a categories table. + * Class name ends in "y" preceded by a consonant or "qu": The "y" is replaced with "ies", + so a Category class becomes a categories table. * Class name ends in "fe": The "fe" is replaced with "ves", so a Wife class becomes a wives table. * Class name ends in "lf" or "rf": The "f" is replaced with "ves", so a Half class becomes a halves table. * Class name ends in "person": The "person" is replaced with "people", so a Salesperson class becomes a salespeople table. @@ -5579,14 +5585,14 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Class name with word compositions: Compositions are underscored, so CreditCard class becomes a credit_cards table. Additionally, the class-level table_name_prefix is prepended to the table_name and the table_name_suffix is appended. So if you have "myapp_" as a prefix, the table name guess for an Account class becomes "myapp_accounts". - + You can also overwrite this class method to allow for unguessable links, such as a Mouse class with a link to a "mice" table. Example: - + class Mouse < ActiveRecord::Base def self.table_name() "mice" end end - + This conversion is now done through an external class called Inflector residing in lib/active_record/support/inflector.rb. * Added find_all_in_collection to has_many defined collections. Works like this: @@ -5594,7 +5600,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio class Firm < ActiveRecord::Base has_many :clients end - + firm.id # => 1 firm.find_all_in_clients "revenue > 1000" # SELECT * FROM clients WHERE firm_id = 1 AND revenue > 1000 @@ -5610,11 +5616,11 @@ in effect. Added :readonly finder constraint. Calling an association collectio errors.add_on_boundry_breaking "password", 3..20 end end - + This will add an error to the tune of "is too short (minimum is 3 characters)" or "is too long (minimum is 20 characters)" if the password is outside the boundry. The messages can be changed by passing a third and forth parameter as message strings. -* Implemented a clone method that works properly with AR. It returns a clone of the record that +* Implemented a clone method that works properly with AR. It returns a clone of the record that hasn't been assigned an id yet and is treated as a new record. * Allow for domain sockets in PostgreSQL by not assuming localhost when no host is specified [Scott Barron] @@ -5645,12 +5651,12 @@ in effect. Added :readonly finder constraint. Calling an association collectio *0.9.3* -* Fixed bug with using a different primary key name together with has_and_belongs_to_many [Investigation by Scott] +* Fixed bug with using a different primary key name together with has_and_belongs_to_many [Investigation by Scott] * Added :exclusively_dependent option to the has_many association macro. The doc reads: If set to true all the associated object are deleted in one SQL statement without having their - before_destroy callback run. This should only be used on associations that depend solely on + before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved. @@ -5662,7 +5668,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Fixed the use of floats (was broken since 0.9.0+) -* Fixed PostgreSQL adapter so default values are displayed properly when used in conjunction with +* Fixed PostgreSQL adapter so default values are displayed properly when used in conjunction with Action Pack scaffolding. * Fixed booleans support for PostgreSQL (use real true/false on boolean fields instead of 0/1 on tinyints) [radsaq] @@ -5688,21 +5694,21 @@ in effect. Added :readonly finder constraint. Calling an association collectio class Event < ActiveRecord::Base has_one_and_belongs_to_many :sponsors end - + class Sponsor < ActiveRecord::Base has_one_and_belongs_to_many :sponsors end Earlier, you'd have to use synthetic methods for creating associations between two objects of the above class: - + roskilde_festival.add_to_sponsors(carlsberg) roskilde_festival.remove_from_sponsors(carlsberg) nike.add_to_events(world_cup) nike.remove_from_events(world_cup) - + Now you can use regular array-styled methods: - + roskilde_festival.sponsors << carlsberg roskilde_festival.sponsors.delete(carlsberg) @@ -5711,7 +5717,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Added delete method for has_many associations. Using this will nullify an association between the has_many and the belonging object by setting the foreign key to null. Consider this model: - + class Post < ActiveRecord::Base has_many :comments end @@ -5732,7 +5738,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio * Active Record is now thread safe! (So you can use it with Cerise and WEBrick applications) [Implementation idea by Michael Neumann, debugging assistance by Jamis Buck] -* Improved performance by roughly 400% on a basic test case of pulling 100 records and querying one attribute. +* Improved performance by roughly 400% on a basic test case of pulling 100 records and querying one attribute. This brings the tax for using Active Record instead of "riding on the metal" (using MySQL-ruby C-driver directly) down to ~50%. Done by doing lazy type conversions and caching column information on the class-level. @@ -5749,46 +5755,46 @@ in effect. Added :readonly finder constraint. Calling an association collectio end Iterating over 100 posts like this: - + <% for post in @posts %> <%= post.title %> has <%= post.comments_count %> comments <% end %> - + Will generate 100 SQL count queries -- one for each call to post.comments_count. If you instead add a "comments_count" int column to the posts table and rewrite the comments association macro with: class Comment < ActiveRecord::Base belongs_to :post, :counter_cache => true end - + Those 100 SQL count queries will be reduced to zero. Beware that counter caching is only appropriate for objects that begin life with the object it's specified to belong with and is destroyed like that as well. Typically objects where you would also specify :dependent => true. If your objects switch from one belonging to another (like a post that can be move from one category to another), - you'll have to manage the counter yourself. + you'll have to manage the counter yourself. * Added natural object-style assignment for has_one and belongs_to associations. Consider the following model: class Project < ActiveRecord::Base has_one :manager end - + class Manager < ActiveRecord::Base belongs_to :project end - + Earlier, assignments would work like following regardless of which way the assignment told the best story: - + active_record.manager_id = david.id - + Now you can do it either from the belonging side: david.project = active_record - + ...or from the having side: - + active_record.manager = david - - If the assignment happens from the having side, the assigned object is automatically saved. So in the example above, the + + If the assignment happens from the having side, the assigned object is automatically saved. So in the example above, the project_id attribute on david would be set to the id of active_record, then david would be saved. * Added natural object-style assignment for has_many associations [Florian Weber]. Consider the following model: @@ -5796,36 +5802,36 @@ in effect. Added :readonly finder constraint. Calling an association collectio class Project < ActiveRecord::Base has_many :milestones end - + class Milestone < ActiveRecord::Base belongs_to :project end - + Earlier, assignments would work like following regardless of which way the assignment told the best story: - + deadline.project_id = active_record.id - + Now you can do it either from the belonging side: deadline.project = active_record - + ...or from the having side: - + active_record.milestones << deadline - + The milestone is automatically saved with the new foreign key. * API CHANGE: Attributes for text (or blob or similar) columns will now have unknown classes stored using YAML instead of using to_s. (Known classes that won't be yamelized are: String, NilClass, TrueClass, FalseClass, Fixnum, Date, and Time). Likewise, data pulled out of text-based attributes will be attempted converged using Yaml if they have the "--- " header. This was primarily done to be enable the storage of hashes and arrays without wrapping them in aggregations, so now you can do: - + user = User.find(1) user.preferences = { "background" => "black", "display" => large } user.save - + User.find(1).preferences # => { "background" => "black", "display" => large } - + Please note that this method should only be used when you don't care about representing the object in proper columns in the database. A money object consisting of an amount and a currency is still a much better fit for a value object done through aggregations than this new option. @@ -5833,14 +5839,14 @@ in effect. Added :readonly finder constraint. Calling an association collectio * POSSIBLE CODE BREAKAGE: As a consequence of the lazy type conversions, it's a bad idea to reference the @attributes hash directly (it always was, but now it's paramount that you don't). If you do, you won't get the type conversion. So to implement new accessors for existing attributes, use read_attribute(attr_name) and write_attribute(attr_name, value) instead. Like this: - + class Song < ActiveRecord::Base # Uses an integer of seconds to hold the length of the song - + def length=(minutes) write_attribute("length", minutes * 60) end - + def length read_attribute("length") / 60 end @@ -5876,7 +5882,7 @@ _Misc_ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the # parenteses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float, # s for String, and a for Array. - + This is incredibly useful for assigning dates from HTML drop-downs of month, year, and day. * Fixed bug with custom primary key column name and Base.find on multiple parameters. @@ -5898,17 +5904,17 @@ _Transactions_ _Mapping_ * Added support for non-integer primary keys [Aredridel/earlier work by Michael Neumann] - + User.find "jdoe" Product.find "PDKEY-INT-12" * Added option to specify naming method for primary key column. ActiveRecord::Base.primary_key_prefix_type can either be set to nil, :table_name, or :table_name_with_underscore. :table_name will assume that Product class has a primary key of "productid" and :table_name_with_underscore will assume "product_id". The default nil will just give "id". - -* Added an overwriteable primary_key method that'll instruct AR to the name of the + +* Added an overwriteable primary_key method that'll instruct AR to the name of the id column [Aredridele/earlier work by Guan Yang] - + class Project < ActiveRecord::Base def self.primary_key() "project_id" end end @@ -5929,8 +5935,8 @@ _Mapping_ _Misc_ * Added freeze call to value object assignments to ensure they remain immutable [Spotted by Gavin Sinclair] - -* Changed interface for specifying observed class in observers. Was OBSERVED_CLASS constant, now is + +* Changed interface for specifying observed class in observers. Was OBSERVED_CLASS constant, now is observed_class() class method. This is more consistant with things like self.table_name(). Works like this: class AuditObserver < ActiveRecord::Observer @@ -5957,20 +5963,20 @@ _Misc_ * Added inheritable callback queues that can ensure that certain callback methods or inline fragments are run throughout the entire inheritance hierarchy. Regardless of whether a descendant overwrites the callback method: - + class Topic < ActiveRecord::Base before_destroy :destroy_author, 'puts "I'm an inline fragment"' end - + Learn more in link:classes/ActiveRecord/Callbacks.html -* Added :dependent option to has_many and has_one, which will automatically destroy associated objects when +* Added :dependent option to has_many and has_one, which will automatically destroy associated objects when the holder is destroyed: - + class Album < ActiveRecord::Base has_many :tracks, :dependent => true end - + All the associated tracks are destroyed when the album is. * Added Base.create as a factory that'll create, save, and return a new object in one step. @@ -5980,7 +5986,7 @@ _Misc_ * Fixed the install.rb to include simple.rb [Spotted by Kevin Bullock] -* Modified block syntax to better follow our code standards outlined in +* Modified block syntax to better follow our code standards outlined in http://www.rubyonrails.org/CodingStandards @@ -5991,7 +5997,7 @@ _Misc_ * Changed adapter-specific connection methods to use centralized ActiveRecord::Base.establish_connection, which is parametized through a config hash with symbol keys instead of a regular parameter list. This will allow for database connections to be opened in a more generic fashion. (Luke) - + NOTE: This requires all *_connections to be updated! Read more in: http://ar.rubyonrails.org/classes/ActiveRecord/Base.html#M000081 @@ -6000,7 +6006,7 @@ _Misc_ * Fixed SQLite adapter so dates are returned as Date objects, not Time objects [Spotted by Gavin Sinclair] -* Fixed requirement of date class, so date conversions are succesful regardless of whether you +* Fixed requirement of date class, so date conversions are succesful regardless of whether you manually require date or not. @@ -6008,10 +6014,10 @@ _Misc_ * Added transactions -* Changed Base.find to also accept either a list (1, 5, 6) or an array of ids ([5, 7]) +* Changed Base.find to also accept either a list (1, 5, 6) or an array of ids ([5, 7]) as parameter and then return an array of objects instead of just an object -* Fixed method has_collection? for has_and_belongs_to_many macro to behave as a +* Fixed method has_collection? for has_and_belongs_to_many macro to behave as a collection, not an association * Fixed SQLite adapter so empty or nil values in columns of datetime, date, or time type diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 12e094b406..392b717e0a 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -49,7 +49,10 @@ end connection_path = "test/connections/#{adapter =~ /jdbc/ ? 'jdbc' : 'native'}_#{adapter}" adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z]+/] t.libs << "test" << connection_path - t.test_files=Dir.glob( "test/cases/**/*_test{,_#{adapter_short}}.rb" ).sort + t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { + |x| x =~ /\/adapters\// + } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort + t.verbose = true t.warning = true } @@ -59,7 +62,9 @@ end adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z]+/] puts [adapter, adapter_short, connection_path].inspect ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) - Dir["test/cases/**/*_test{,_#{adapter_short}}.rb"].all? do |file| + (Dir["test/cases/**/*_test.rb"].reject { + |x| x =~ /\/adapters\// + } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| system(ruby, "-Ilib:test:#{connection_path}", file) end or raise "Failures" end diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index f69576b240..f7d358337c 100755..100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -116,15 +116,15 @@ RBench.run(TIMES) do end report 'Model.all limit(100)', (TIMES / 10).ceil do - ar { Exhibit.look Exhibit.all(:limit => 100) } + ar { Exhibit.look Exhibit.limit(100) } end report 'Model.all limit(100) with relationship', (TIMES / 10).ceil do - ar { Exhibit.feel Exhibit.all(:limit => 100, :include => :user) } + ar { Exhibit.feel Exhibit.limit(100).includes(:user) } end report 'Model.all limit(10,000)', (TIMES / 1000).ceil do - ar { Exhibit.look Exhibit.all(:limit => 10000) } + ar { Exhibit.look Exhibit.limit(10000) } end exhibit = { diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 45aaea062d..c45400d3d9 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -1,4 +1,5 @@ module ActiveRecord + # = Active Record Aggregations module Aggregations # :nodoc: extend ActiveSupport::Concern @@ -189,7 +190,7 @@ module ActiveRecord # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) }, # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) } # - def composed_of(part_id, options = {}, &block) + def composed_of(part_id, options = {}) options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) name = part_id.id2name @@ -198,9 +199,7 @@ module ActiveRecord mapping = [ mapping ] unless mapping.first.is_a?(Array) allow_nil = options[:allow_nil] || false constructor = options[:constructor] || :new - converter = options[:converter] || block - - ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given? + converter = options[:converter] reader_method(name, class_name, mapping, allow_nil, constructor) writer_method(name, class_name, mapping, allow_nil, converter) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 9b59266bbc..4caa434fc0 100755..100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -88,8 +88,8 @@ module ActiveRecord end end - # This error is raised when trying to destroy a parent instance in a N:1, 1:1 assosications - # (has_many, has_one) when there is at least 1 child assosociated instance. + # This error is raised when trying to destroy a parent instance in N:1 or 1:1 associations + # (has_many, has_one) when there is at least 1 child associated instance. # ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project class DeleteRestrictionError < ActiveRecordError #:nodoc: def initialize(reflection) @@ -886,11 +886,13 @@ module ActiveRecord # [:validate] # If false, don't validate the associated objects when saving the parent object. true by default. # [:autosave] - # If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default. + # If true, always save the associated objects or destroy them if marked for destruction, when saving the parent object. + # If false, never save or destroy the associated objects. + # By default, only save associated objects that are new records. # [:inverse_of] # Specifies the name of the <tt>belongs_to</tt> association on the associated object that is the inverse of this <tt>has_many</tt> # association. Does not work in combination with <tt>:through</tt> or <tt>:as</tt> options. - # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional assocations for more detail. + # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: # has_many :comments, :order => "posted_on" @@ -1001,11 +1003,13 @@ module ActiveRecord # [:validate] # If false, don't validate the associated object when saving the parent object. +false+ by default. # [:autosave] - # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default. + # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. + # If false, never save or destroy the associated object. + # By default, only save the associated object if it's a new record. # [:inverse_of] # Specifies the name of the <tt>belongs_to</tt> association on the associated object that is the inverse of this <tt>has_one</tt> # association. Does not work in combination with <tt>:through</tt> or <tt>:as</tt> options. - # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional assocations for more detail. + # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card @@ -1103,14 +1107,16 @@ module ActiveRecord # [:validate] # If false, don't validate the associated objects when saving the parent object. +false+ by default. # [:autosave] - # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default. + # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. + # If false, never save or destroy the associated object. + # By default, only save the associated object if it's a new record. # [:touch] # If true, the associated object will be touched (the updated_at/on attributes set to now) when this record is either saved or # destroyed. If you specify a symbol, that attribute will be updated with the current time instead of the updated_at/on attribute. # [:inverse_of] # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated object that is the inverse of this <tt>belongs_to</tt> # association. Does not work in combination with the <tt>:polymorphic</tt> options. - # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional assocations for more detail. + # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -1290,7 +1296,9 @@ module ActiveRecord # [:validate] # If false, don't validate the associated objects when saving the parent object. +true+ by default. # [:autosave] - # If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default. + # If true, always save the associated objects or destroy them if marked for destruction, when saving the parent object. + # If false, never save or destroy the associated objects. + # By default, only save associated objects that are new records. # # Option examples: # has_and_belongs_to_many :projects @@ -2066,7 +2074,7 @@ module ActiveRecord unless klass.descends_from_active_record? sti_column = aliased_table[klass.inheritance_column] sti_condition = sti_column.eq(klass.sti_name) - klass.send(:subclasses).each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) } + klass.descendants.each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) } @join << sti_condition end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index d9903243ce..ddf4ce4058 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -3,6 +3,8 @@ require 'active_support/core_ext/array/wrap' module ActiveRecord module Associations + # = Active Record Association Collection + # # AssociationCollection is an abstract class that provides common stuff to # ease the implementation of association proxies that represent # collections. See the class hierarchy in AssociationProxy. @@ -24,10 +26,10 @@ module ActiveRecord delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped - def select(select = nil, &block) + def select(select = nil) if block_given? load_target - @target.select(&block) + @target.select.each { |e| yield e } else scoped.select(select) end @@ -121,7 +123,7 @@ module ActiveRecord end end - # Add +records+ to this association. Returns +self+ so method calls may be chained. + # Add +records+ to this association. Returns +self+ so method calls may be chained. # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. def <<(*records) result = true @@ -166,7 +168,7 @@ module ActiveRecord reset_target! reset_named_scopes_cache! end - + # Calculate sum using SQL, not Enumerable def sum(*args) if block_given? @@ -239,7 +241,7 @@ module ActiveRecord if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy destroy_all - else + else delete_all end @@ -388,7 +390,11 @@ module ActiveRecord begin if !loaded? if @target.is_a?(Array) && @target.any? - @target = find_target + @target.find_all {|t| t.new_record? } + @target = find_target.map do |f| + i = @target.index(f) + t = @target.delete_at(i) if i + (t && t.changed?) ? t : f + end + @target else @target = find_target end @@ -403,6 +409,17 @@ module ActiveRecord end def method_missing(method, *args) + case method.to_s + when 'find_or_create' + return find(:first, :conditions => args.first) || create(args.first) + when /^find_or_create_by_(.*)$/ + rest = $1 + return send("find_by_#{rest}", *args) || + method_missing("create_by_#{rest}", *args) + when /^create_by_(.*)$/ + return create Hash[$1.split('_and_').zip(args)] + end + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) if block_given? super { |*block_args| yield(*block_args) } @@ -514,8 +531,8 @@ module ActiveRecord def callbacks_for(callback_name) full_callback_name = "#{callback_name}_for_#{@reflection.name}" @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || [] - end - + end + def ensure_owner_is_not_new if @owner.new_record? raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index e88618d278..f333f4d603 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -2,6 +2,8 @@ require 'active_support/core_ext/array/wrap' module ActiveRecord module Associations + # = Active Record Associations + # # This is the root class of all association proxies: # # AssociationProxy diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index d2f2267e5c..c2a6495db5 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -1,4 +1,5 @@ module ActiveRecord + # = Active Record Belongs To Associations module Associations class BelongsToAssociation < AssociationProxy #:nodoc: def create(attributes = {}) diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index f6edd6383c..38454ec242 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -1,4 +1,5 @@ module ActiveRecord + # = Active Record Belongs To Polymorphic Association module Associations class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc: def replace(record) diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 7f39a189e4..c989c3536d 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -1,4 +1,5 @@ module ActiveRecord + # = Active Record Has And Belongs To Many Association module Associations class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: def create(attributes = {}) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 0464e8ceea..d74fb7c702 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -1,4 +1,5 @@ module ActiveRecord + # = Active Record Has Many Association module Associations # This is the proxy that handles a has many association. # @@ -109,7 +110,11 @@ module ActiveRecord create_scoping = {} set_belongs_to_association_for(create_scoping) { - :find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit], :include => @reflection.options[:include]}, + :find => { :conditions => @finder_sql, + :readonly => false, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :include => @reflection.options[:include]}, :create => create_scoping } end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 5338bb099d..17f850756f 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -2,6 +2,7 @@ require "active_record/associations/through_association_scope" require 'active_support/core_ext/object/blank' module ActiveRecord + # = Active Record Has Many Through Association module Associations class HasManyThroughAssociation < HasManyAssociation #:nodoc: include ThroughAssociationScope diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index ea769fd48b..68b8b792ad 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,4 +1,5 @@ module ActiveRecord + # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < AssociationProxy #:nodoc: def initialize(owner, reflection) diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index a79bf943d1..fba0a2bfcc 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -1,6 +1,7 @@ require "active_record/associations/through_association_scope" module ActiveRecord + # = Active Record Has One Through Association module Associations class HasOneThroughAssociation < HasOneAssociation include ThroughAssociationScope diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 1d2f323112..22e1033a9d 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -1,4 +1,5 @@ module ActiveRecord + # = Active Record Through Association Scope module Associations module ThroughAssociationScope @@ -91,7 +92,7 @@ module ActiveRecord # Construct attributes for :through pointing to owner and associate. def construct_join_attributes(associate) - # TODO: revist this to allow it for deletion, supposing dependent option is supported + # TODO: revisit this to allow it for deletion, supposing dependent option is supported raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index c117271c71..56e18eced0 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/enumerable' module ActiveRecord + # = Active Record Attribute Methods module AttributeMethods #:nodoc: extend ActiveSupport::Concern include ActiveModel::AttributeMethods diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 0dcadfab5f..7517896235 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -1,6 +1,8 @@ require 'active_support/core_ext/array/wrap' module ActiveRecord + # = Active Record Autosave Association + # # AutosaveAssociation is a module that takes care of automatically saving # your associations when the parent is saved. In addition to saving, it # also destroys any associations that were marked for destruction. @@ -145,12 +147,12 @@ module ActiveRecord # add_autosave_association_callbacks(reflect_on_association(name)) # end ASSOCIATION_TYPES.each do |type| - module_eval %{ + module_eval <<-CODE, __FILE__, __LINE__ + 1 def #{type}(name, options = {}) super add_autosave_association_callbacks(reflect_on_association(name)) end - } + CODE end # Adds a validate and save callback for the association as specified by @@ -375,7 +377,7 @@ module ActiveRecord if association.updated? association_id = association.send(reflection.options[:primary_key] || :id) self[reflection.primary_key_name] = association_id - # TODO: Removing this code doesn't seem to matter… + # TODO: Removing this code doesn't seem to matter... if reflection.options[:polymorphic] self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 3b6ffa46f2..c8795e4496 100755..100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -2,6 +2,7 @@ require 'yaml' require 'set' require 'active_support/benchmarkable' require 'active_support/dependencies' +require 'active_support/descendants_tracker' require 'active_support/time' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute_accessors' @@ -14,13 +15,17 @@ require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/delegation' +require 'active_support/core_ext/module/deprecation' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/object/blank' require 'arel' require 'active_record/errors' +require 'active_record/log_subscriber' module ActiveRecord #:nodoc: + # = Active Record + # # Active Record objects don't specify their attributes directly, but rather infer them from the table definition with # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change # is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain @@ -274,27 +279,17 @@ module ActiveRecord #:nodoc: # on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+. cattr_accessor :logger, :instance_writer => false - def self.inherited(child) #:nodoc: - @@subclasses[self] ||= [] - @@subclasses[self] << child - super - end + class << self + def reset_subclasses #:nodoc: + ActiveSupport::Deprecation.warn 'ActiveRecord::Base.reset_subclasses no longer does anything in Rails 3. It will be removed in the final release; please update your apps and plugins.', caller + end - def self.reset_subclasses #:nodoc: - nonreloadables = [] - subclasses.each do |klass| - unless ActiveSupport::Dependencies.autoloaded? klass - nonreloadables << klass - next - end - klass.instance_variables.each { |var| klass.send(:remove_instance_variable, var) } - klass.instance_methods(false).each { |m| klass.send :undef_method, m } + def subclasses + descendants end - @@subclasses = {} - nonreloadables.each { |klass| (@@subclasses[klass.superclass] ||= []) << klass } - end - @@subclasses = {} + deprecate :subclasses => :descendants + end ## # :singleton-method: @@ -403,7 +398,7 @@ module ActiveRecord #:nodoc: delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped delegate :find_each, :find_in_batches, :to => :scoped - delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped + delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped # Executes a custom SQL query against your database and returns all the results. The results will @@ -725,14 +720,6 @@ module ActiveRecord #:nodoc: end alias :sequence_name= :set_sequence_name - # Turns the +table_name+ back into a class name following the reverse rules of +table_name+. - def class_name(table_name = table_name) # :nodoc: - # remove any prefix and/or suffix from the table name - class_name = table_name[table_name_prefix.length..-(table_name_suffix.length + 1)].camelize - class_name = class_name.singularize if pluralize_table_names - class_name - end - # Indicates whether the table associated with this class exists def table_exists? connection.table_exists?(table_name) @@ -806,11 +793,11 @@ module ActiveRecord #:nodoc: def reset_column_information undefine_attribute_methods @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil - @arel_engine = @unscoped = @arel_table = nil + @arel_engine = @relation = @arel_table = nil end def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc: - subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } + descendants.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } end def attribute_method?(attribute) @@ -909,9 +896,9 @@ module ActiveRecord #:nodoc: store_full_sti_class ? name : name.demodulize end - def unscoped - @unscoped ||= Relation.new(self, arel_table) - finder_needs_type_condition? ? @unscoped.where(type_condition) : @unscoped + def relation + @relation ||= Relation.new(self, arel_table) + finder_needs_type_condition? ? @relation.where(type_condition) : @relation end def arel_table @@ -928,6 +915,31 @@ module ActiveRecord #:nodoc: end end + # Returns a scope for this class without taking into account the default_scope. + # + # class Post < ActiveRecord::Base + # default_scope :published => true + # end + # + # Post.all # Fires "SELECT * FROM posts WHERE published = true" + # Post.unscoped.all # Fires "SELECT * FROM posts" + # + # This method also accepts a block meaning that all queries inside the block will + # not use the default_scope: + # + # Post.unscoped { + # limit(10) # Fires "SELECT * FROM posts LIMIT 10" + # } + # + def unscoped + block_given? ? relation.scoping { yield } : relation + end + + def scoped_methods #:nodoc: + key = :"#{self}_scoped_methods" + Thread.current[key] = Thread.current[key].presence || self.default_scoping.dup + end + private # Finder methods must instantiate through this method to work with the # single-table inheritance model that makes it possible to create @@ -935,8 +947,8 @@ module ActiveRecord #:nodoc: def instantiate(record) object = find_sti_class(record[inheritance_column]).allocate - object.instance_variable_set(:'@attributes', record) - object.instance_variable_set(:'@attributes_cache', {}) + object.instance_variable_set(:@attributes, record) + object.instance_variable_set(:@attributes_cache, {}) object.instance_variable_set(:@new_record, false) object.instance_variable_set(:@readonly, false) object.instance_variable_set(:@destroyed, false) @@ -975,7 +987,7 @@ module ActiveRecord #:nodoc: def type_condition sti_column = arel_table[inheritance_column] condition = sti_column.eq(sti_name) - subclasses.each{|subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) } + descendants.each { |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) } condition end @@ -1162,16 +1174,22 @@ module ActiveRecord #:nodoc: # Works like with_scope, but discards any nested properties. def with_exclusive_scope(method_scoping = {}, &block) - with_scope(method_scoping, :overwrite, &block) - end + if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) } + raise ArgumentError, <<-MSG +New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope: - # Returns a list of all subclasses of this class, meaning all descendants. - def subclasses - @@subclasses[self] ||= [] - @@subclasses[self] + @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses } - end + User.unscoped.where(:active => true) - public :subclasses +Or call unscoped with a block: + + User.unscoped do + User.where(:active => true).all + end + +MSG + end + with_scope(method_scoping, :overwrite, &block) + end # Sets the default options for the model. The format of the # <tt>options</tt> argument is the same as in find. @@ -1183,11 +1201,6 @@ module ActiveRecord #:nodoc: self.default_scoping << construct_finder_arel(options, default_scoping.pop) end - def scoped_methods #:nodoc: - key = :"#{self}_scoped_methods" - Thread.current[key] = Thread.current[key].presence || self.default_scoping.dup - end - def current_scoped_methods #:nodoc: scoped_methods.last end @@ -1440,14 +1453,6 @@ module ActiveRecord #:nodoc: # as it copies the object's attributes only, not its associations. The extent of a "deep" clone is # application specific and is therefore left to the application to implement according to its need. def initialize_copy(other) - # Think the assertion which fails if the after_initialize callback goes at the end of the method is wrong. The - # deleted clone method called new which therefore called the after_initialize callback. It then went on to copy - # over the attributes. But if it's copying the attributes afterwards then it hasn't finished initializing right? - # For example in the test suite the topic model's after_initialize method sets the author_email_address to - # test@test.com. I would have thought this would mean that all cloned models would have an author email address - # of test@test.com. However the test_clone test method seems to test that this is not the case. As a result the - # after_initialize callback has to be run *before* the copying of the atrributes rather than afterwards in order - # for all tests to pass. This makes no sense to me. callback(:after_initialize) if respond_to_without_attributes?(:after_initialize) cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) cloned_attributes.delete(self.class.primary_key) @@ -1460,6 +1465,7 @@ module ActiveRecord #:nodoc: end clear_aggregation_cache + clear_association_cache @attributes_cache = {} @new_record = true ensure_proper_type @@ -1900,6 +1906,7 @@ module ActiveRecord #:nodoc: extend ActiveModel::Naming extend QueryCache::ClassMethods extend ActiveSupport::Benchmarkable + extend ActiveSupport::DescendantsTracker include ActiveModel::Conversion include Validations diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 44fee12001..637dac450b 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -1,6 +1,8 @@ require 'active_support/core_ext/array/wrap' module ActiveRecord + # = Active Record Callbacks + # # Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic # before or after an alteration of the object state. This can be used to make sure that associated and # dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes @@ -234,8 +236,7 @@ module ActiveRecord included do extend ActiveModel::Callbacks - - define_callbacks :validation, :terminator => "result == false", :scope => [:kind, :name] + include ActiveModel::Validations::Callbacks define_model_callbacks :initialize, :find, :only => :after define_model_callbacks :save, :create, :update, :destroy @@ -249,29 +250,6 @@ module ActiveRecord send(meth.to_sym, meth.to_sym) end end - - def before_validation(*args, &block) - options = args.last - if options.is_a?(Hash) && options[:on] - options[:if] = Array.wrap(options[:if]) - options[:if] << "@_on_validate == :#{options[:on]}" - end - set_callback(:validation, :before, *args, &block) - end - - def after_validation(*args, &block) - options = args.extract_options! - options[:prepend] = true - options[:if] = Array.wrap(options[:if]) - options[:if] << "!halted && value != false" - options[:if] << "@_on_validate == :#{options[:on]}" if options[:on] - set_callback(:validation, :after, *(args << options), &block) - end - end - - def valid?(*) #:nodoc: - @_on_validate = new_record? ? :create : :update - _run_validation_callbacks { super } end def destroy #:nodoc: @@ -286,6 +264,7 @@ module ActiveRecord end private + def create_or_update #:nodoc: _run_save_callbacks { super } end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 979ed52f4a..c2d79a421d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -144,7 +144,9 @@ module ActiveRecord @connections.each do |conn| conn.disconnect! if conn.requires_reloading? end - @connections = [] + @connections.delete_if do |conn| + conn.requires_reloading? + end end # Verify active connections and remove and disconnect connections diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index b9fb452eee..25432e9985 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -304,7 +304,7 @@ module ActiveRecord begin record.rolledback!(rollback) rescue Exception => e - record.logger.error(e) if record.respond_to?(:logger) + record.logger.error(e) if record.respond_to?(:logger) && record.logger end end end @@ -319,7 +319,7 @@ module ActiveRecord begin record.committed! rescue Exception => e - record.logger.error(e) if record.respond_to?(:logger) + record.logger.error(e) if record.respond_to?(:logger) && record.logger end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 7d58bc2adf..7691b6a788 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -582,6 +582,11 @@ module ActiveRecord @base.add_column(@table_name, column_name, type, options) end + # Checks to see if a column exists. See SchemaStatements#column_exists? + def column_exists?(column_name, type = nil, options = nil) + @base.column_exists?(@table_name, column_name, type, options) + end + # Adds a new index to the table. +column_name+ can be a single Symbol, or # an Array of Symbols. See SchemaStatements#add_index # @@ -596,6 +601,11 @@ module ActiveRecord @base.add_index(@table_name, column_name, options) end + # Checks to see if an index exists. See SchemaStatements#index_exists? + def index_exists?(column_name, options = {}) + @base.index_exists?(@table_name, column_name, options) + end + # Adds timestamps (created_at and updated_at) columns to the table. See SchemaStatements#add_timestamps # ===== Example # t.timestamps diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index d3499cea72..76b65bf219 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -24,10 +24,53 @@ module ActiveRecord # Returns an array of indexes for the given table. # def indexes(table_name, name = nil) end + # Checks to see if an index exists on a table for a given index definition + # + # === Examples + # # Check an index exists + # index_exists?(:suppliers, :company_id) + # + # # Check an index on multiple columns exists + # index_exists?(:suppliers, [:company_id, :company_type]) + # + # # Check a unique index exists + # index_exists?(:suppliers, :company_id, :unique => true) + # + # # Check an index with a custom name exists + # index_exists?(:suppliers, :company_id, :name => "idx_company_id" + def index_exists?(table_name, column_name, options = {}) + column_names = Array.wrap(column_name) + index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) + if options[:unique] + indexes(table_name).any?{ |i| i.unique && i.name == index_name } + else + indexes(table_name).any?{ |i| i.name == index_name } + end + end + # Returns an array of Column objects for the table specified by +table_name+. # See the concrete implementation for details on the expected parameter values. def columns(table_name, name = nil) end + # Checks to see if a column exists in a given table. + # + # === Examples + # # Check a column exists + # column_exists?(:suppliers, :name) + # + # # Check a column exists of a particular type + # column_exists?(:suppliers, :name, :string) + # + # # Check a column exists with a specific definition + # column_exists?(:suppliers, :name, :string, :limit => 100) + def column_exists?(table_name, column_name, type = nil, options = {}) + columns(table_name).any?{ |c| c.name == column_name.to_s && + (!type || c.type == type) && + (!options[:limit] || c.limit == options[:limit]) && + (!options[:precision] || c.precision == options[:precision]) && + (!options[:scale] || c.scale == options[:scale]) } + end + # Creates a new table with the name +table_name+. +table_name+ may either # be a String or a Symbol. # @@ -205,6 +248,7 @@ module ActiveRecord # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? column_names.flatten.each do |column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" end @@ -292,7 +336,7 @@ module ActiveRecord @logger.warn("Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters. Skipping.") return end - if index_exists?(table_name, index_name, false) + if index_name_exists?(table_name, index_name, false) @logger.warn("Index name '#{index_name}' on table '#{table_name}' already exists. Skipping.") return end @@ -313,7 +357,7 @@ module ActiveRecord # remove_index :accounts, :name => :by_branch_party def remove_index(table_name, options = {}) index_name = index_name(table_name, options) - unless index_exists?(table_name, index_name, true) + unless index_name_exists?(table_name, index_name, true) @logger.warn("Index name '#{index_name}' on table '#{table_name}' does not exist. Skipping.") return end @@ -321,7 +365,7 @@ module ActiveRecord end def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_column_name(index_name)} ON #{table_name}" + execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" end # Rename an index. @@ -350,11 +394,11 @@ module ActiveRecord end end - # Verify the existence of an index. + # Verify the existence of an index with a given name. # # The default argument is returned if the underlying implementation does not define the indexes method, # as there's no way to determine the correct answer in that case. - def index_exists?(table_name, index_name, default) + def index_name_exists?(table_name, index_name, default) return default unless respond_to?(:indexes) indexes(table_name).detect { |i| i.name == index_name } end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 4ee9fee4a9..be8d1bd76b 100755..100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -107,7 +107,7 @@ module ActiveRecord # REFERENTIAL INTEGRITY ==================================== # Override to turn off referential integrity while executing <tt>&block</tt>. - def disable_referential_integrity(&block) + def disable_referential_integrity yield end @@ -142,9 +142,10 @@ module ActiveRecord # this should be overridden by concrete adapters end - # Returns true if its safe to reload the connection between requests for development mode. + # Returns true if its required to reload the connection between requests for development mode. + # This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite. def requires_reloading? - true + false end # Checks whether the connection to the database is still active (i.e. not stale). diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 7c7bc5e292..aa3626a37e 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -219,7 +219,7 @@ module ActiveRecord # REFERENTIAL INTEGRITY ==================================== - def disable_referential_integrity(&block) #:nodoc: + def disable_referential_integrity #:nodoc: old = select_value("SELECT @@FOREIGN_KEY_CHECKS") begin diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e84242601b..2fe2ae7136 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -216,7 +216,10 @@ module ActiveRecord super(connection, logger) @connection_parameters, @config = connection_parameters, config + # @local_tz is initialized as nil to avoid warnings when connect tries to use it + @local_tz = nil connect + @local_tz = execute('SHOW TIME ZONE').first["TimeZone"] end # Is this connection alive and ready for queries? @@ -372,7 +375,7 @@ module ActiveRecord return false end - def disable_referential_integrity(&block) #:nodoc: + def disable_referential_integrity #:nodoc: if supports_disable_referential_integrity?() then execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) end @@ -606,27 +609,22 @@ module ActiveRecord SQL - indexes = [] - - indexes = result.map do |row| + result.map do |row| index_name = row[0] unique = row[1] == 't' indkey = row[2].split(" ") oid = row[3] - columns = query(<<-SQL, "Columns for index #{row[0]} on #{table_name}").inject({}) {|attlist, r| attlist[r[1]] = r[0]; attlist} - SELECT a.attname, a.attnum + columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")] + SELECT a.attnum, a.attname FROM pg_attribute a WHERE a.attrelid = #{oid} AND a.attnum IN (#{indkey.join(",")}) SQL - column_names = indkey.map {|attnum| columns[attnum] } - IndexDefinition.new(table_name, index_name, unique, column_names) - - end - - indexes + column_names = columns.values_at(*indkey).compact + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names) + end.compact end # Returns the list of all column definitions for a table. @@ -929,9 +927,8 @@ module ActiveRecord # TIMESTAMP WITH ZONE types in UTC. if ActiveRecord::Base.default_timezone == :utc execute("SET time zone 'UTC'") - else - offset = Time.local(2000).utc_offset / 3600 - execute("SET time zone '#{offset}'") + elsif @local_tz + execute("SET time zone '#{@local_tz}'") end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index f295af16f0..0d9a86a1ea 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -6,6 +6,10 @@ module ActiveRecord def self.sqlite3_connection(config) # :nodoc: parse_sqlite_config!(config) + unless 'sqlite3' == config[:adapter] + raise ArgumentError, 'adapter name should be "sqlite3"' + end + unless self.class.const_defined?(:SQLite3) require_library_or_gem(config[:adapter]) end @@ -24,13 +28,13 @@ module ActiveRecord module ConnectionAdapters #:nodoc: class SQLite3Adapter < SQLiteAdapter # :nodoc: - + # Returns the current database encoding format as a string, eg: 'UTF-8' def encoding if @connection.respond_to?(:encoding) - @connection.encoding[0]['encoding'] + @connection.encoding.to_s else - encoding = @connection.send(:get_query_pragma, 'encoding') + encoding = @connection.execute('PRAGMA encoding') encoding[0]['encoding'] end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index deb62e3802..1927585c49 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -151,7 +151,7 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== def execute(sql, name = nil) #:nodoc: - catch_schema_changes { log(sql, name) { @connection.execute(sql) } } + log(sql, name) { @connection.execute(sql) } end def update_sql(sql, name = nil) #:nodoc: @@ -176,15 +176,15 @@ module ActiveRecord end def begin_db_transaction #:nodoc: - catch_schema_changes { @connection.transaction } + @connection.transaction end def commit_db_transaction #:nodoc: - catch_schema_changes { @connection.commit } + @connection.commit end def rollback_db_transaction #:nodoc: - catch_schema_changes { @connection.rollback } + @connection.rollback end # SCHEMA STATEMENTS ======================================== @@ -246,6 +246,7 @@ module ActiveRecord end def remove_column(table_name, *column_names) #:nodoc: + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? column_names.flatten.each do |column_name| alter_table(table_name) do |definition| definition.columns.delete(definition[column_name]) @@ -390,17 +391,6 @@ module ActiveRecord end end - def catch_schema_changes - return yield - rescue ActiveRecord::StatementInvalid => exception - if exception.message =~ /database schema has changed/ - reconnect! - retry - else - raise - end - end - def sqlite_version @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)')) end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 9601ed6afd..999322129a 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -1,4 +1,5 @@ module ActiveRecord + # = Active Record Counter Cache module CounterCache # Resets one or more counter caches to their correct value using an SQL # count query. This is useful when adding new counter caches, or if the diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb index fa7a19487c..b39b291352 100644 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ b/activerecord/lib/active_record/dynamic_finder_match.rb @@ -1,4 +1,9 @@ module ActiveRecord + + # = Active Record Dynamic Finder Match + # + # Provides dynamic attribute-based finders such as <tt>find_by_country</tt> + # if, for example, the <tt>Person</tt> has an attribute with that name. class DynamicFinderMatch def self.match(method) df_match = self.new(method) diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb index f796ba669a..15f65be6bc 100644 --- a/activerecord/lib/active_record/dynamic_scope_match.rb +++ b/activerecord/lib/active_record/dynamic_scope_match.rb @@ -1,4 +1,11 @@ module ActiveRecord + + # = Active Record Dynamic Scope Match + # + # Provides dynamic attribute-based scopes such as <tt>scoped_by_price(4.99)</tt> + # if, for example, the <tt>Product</tt> has an attribute with that name. You can + # chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named + # scope except that it's dynamic. class DynamicScopeMatch def self.match(method) ds_match = self.new(method) diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index cf5ddca2ba..7aa725d095 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -1,4 +1,7 @@ module ActiveRecord + + # = Active Record Errors + # # Generic Active Record exception class. class ActiveRecordError < StandardError end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb new file mode 100644 index 0000000000..71065f9908 --- /dev/null +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -0,0 +1,32 @@ +module ActiveRecord + class LogSubscriber < ActiveSupport::LogSubscriber + def initialize + super + @odd_or_even = false + end + + def sql(event) + name = '%s (%.1fms)' % [event.payload[:name], event.duration] + sql = event.payload[:sql].squeeze(' ') + + if odd? + name = color(name, :cyan, true) + sql = color(sql, nil, true) + else + name = color(name, :magenta, true) + end + + debug " #{name} #{sql}" + end + + def odd? + @odd_or_even = !@odd_or_even + end + + def logger + ActiveRecord::Base.logger + end + end +end + +ActiveRecord::LogSubscriber.attach_to :active_record
\ No newline at end of file diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 940f825038..4c5e1ae218 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/module/aliasing' module ActiveRecord # Exception that can be raised to stop migrations from going backwards. @@ -29,11 +30,15 @@ module ActiveRecord end end - # Migrations can manage the evolution of a schema used by several physical databases. It's a solution - # to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to - # push that change to other developers and to the production server. With migrations, you can describe the transformations - # in self-contained classes that can be checked into version control systems and executed against another database that - # might be one, two, or five versions behind. + # = Active Record Migrations + # + # Migrations can manage the evolution of a schema used by several physical + # databases. It's a solution to the common problem of adding a field to make + # a new feature work in your local database, but being unsure of how to + # push that change to other developers and to the production server. With + # migrations, you can describe the transformations in self-contained classes + # that can be checked into version control systems and executed against + # another database that might be one, two, or five versions behind. # # Example of a simple migration: # @@ -47,10 +52,13 @@ module ActiveRecord # end # end # - # This migration will add a boolean flag to the accounts table and remove it if you're backing out of the migration. - # It shows how all migrations have two class methods +up+ and +down+ that describes the transformations required to implement - # or remove the migration. These methods can consist of both the migration specific methods like add_column and remove_column, - # but may also contain regular Ruby code for generating data needed for the transformations. + # This migration will add a boolean flag to the accounts table and remove it + # if you're backing out of the migration. It shows how all migrations have + # two class methods +up+ and +down+ that describes the transformations + # required to implement or remove the migration. These methods can consist + # of both the migration specific methods like add_column and remove_column, + # but may also contain regular Ruby code for generating data needed for the + # transformations. # # Example of a more complex migration that also needs to initialize data: # @@ -64,7 +72,9 @@ module ActiveRecord # t.integer :position # end # - # SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1 + # SystemSetting.create :name => "notice", + # :label => "Use notice?", + # :value => 1 # end # # def self.down @@ -72,35 +82,49 @@ module ActiveRecord # end # end # - # This migration first adds the system_settings table, then creates the very first row in it using the Active Record model - # that relies on the table. It also uses the more advanced create_table syntax where you can specify a complete table schema - # in one block call. + # This migration first adds the system_settings table, then creates the very + # first row in it using the Active Record model that relies on the table. It + # also uses the more advanced create_table syntax where you can specify a + # complete table schema in one block call. # # == Available transformations # - # * <tt>create_table(name, options)</tt> Creates a table called +name+ and makes the table object available to a block - # that can then add columns to it, following the same format as add_column. See example above. The options hash is for - # fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create table definition. + # * <tt>create_table(name, options)</tt> Creates a table called +name+ and + # makes the table object available to a block that can then add columns to it, + # following the same format as add_column. See example above. The options hash + # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create + # table definition. # * <tt>drop_table(name)</tt>: Drops the table called +name+. - # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ to +new_name+. - # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column to the table called +table_name+ + # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ + # to +new_name+. + # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column + # to the table called +table_name+ # named +column_name+ specified to be one of the following types: - # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, - # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be specified by passing an - # +options+ hash like <tt>{ :default => 11 }</tt>. Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. <tt>{ :limit => 50, :null => false }</tt>) - # -- see ActiveRecord::ConnectionAdapters::TableDefinition#column for details. - # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames a column but keeps the type and content. - # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same - # parameters as add_column. - # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named +column_name+ from the table called +table_name+. - # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index with the name of the column. Other options include - # <tt>:name</tt> and <tt>:unique</tt> (e.g. <tt>{ :name => "users_name_index", :unique => true }</tt>). - # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified by +index_name+. + # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, + # <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, + # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be + # specified by passing an +options+ hash like <tt>{ :default => 11 }</tt>. + # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. + # <tt>{ :limit => 50, :null => false }</tt>) -- see + # ActiveRecord::ConnectionAdapters::TableDefinition#column for details. + # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames + # a column but keeps the type and content. + # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes + # the column to a different type using the same parameters as add_column. + # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named + # +column_name+ from the table called +table_name+. + # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index + # with the name of the column. Other options include + # <tt>:name</tt> and <tt>:unique</tt> (e.g. + # <tt>{ :name => "users_name_index", :unique => true }</tt>). + # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified + # by +index_name+. # # == Irreversible transformations # - # Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise - # an <tt>ActiveRecord::IrreversibleMigration</tt> exception in their +down+ method. + # Some transformations are destructive in a manner that cannot be reversed. + # Migrations of that kind should raise an <tt>ActiveRecord::IrreversibleMigration</tt> + # exception in their +down+ method. # # == Running migrations from within Rails # @@ -110,13 +134,15 @@ module ActiveRecord # rails generate migration MyNewMigration # # where MyNewMigration is the name of your migration. The generator will - # create an empty migration file <tt>timestamp_my_new_migration.rb</tt> in the <tt>db/migrate/</tt> - # directory where <tt>timestamp</tt> is the UTC formatted date and time that the migration was generated. + # create an empty migration file <tt>timestamp_my_new_migration.rb</tt> + # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the + # UTC formatted date and time that the migration was generated. # # You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of # MyNewMigration. # # There is a special syntactic shortcut to generate migrations that add fields to a table. + # # rails generate migration add_fieldname_to_tablename fieldname:string # # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: @@ -191,9 +217,10 @@ module ActiveRecord # # == Using a model after changing its table # - # Sometimes you'll want to add a column in a migration and populate it immediately after. In that case, you'll need - # to make a call to Base#reset_column_information in order to ensure that the model has the latest column data from - # after the new column was added. Example: + # Sometimes you'll want to add a column in a migration and populate it + # immediately after. In that case, you'll need to make a call to + # <tt>Base#reset_column_information</tt> in order to ensure that the model has the + # latest column data from after the new column was added. Example: # # class AddPeopleSalary < ActiveRecord::Migration # def self.up diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 3d8f4a030b..c010dac64e 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -4,11 +4,12 @@ require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/blank' module ActiveRecord + # = Active Record Named \Scopes module NamedScope extend ActiveSupport::Concern module ClassMethods - # Returns an anonymous scope. + # Returns an anonymous \scope. # # posts = Post.scoped # posts.size # Fires "select count(*) from posts" and returns the count @@ -18,14 +19,15 @@ module ActiveRecord # fruits = fruits.where(:colour => 'red') if options[:red_only] # fruits = fruits.limit(10) if limited? # - # Anonymous \scopes tend to be useful when procedurally generating complex queries, where passing - # intermediate values (scopes) around as first-class objects is convenient. + # Anonymous \scopes tend to be useful when procedurally generating complex + # queries, where passing intermediate values (\scopes) around as first-class + # objects is convenient. # - # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope. - def scoped(options = {}, &block) + # You can define a \scope that applies to all finders using + # ActiveRecord::Base.default_scope. + def scoped(options = nil) if options.present? - relation = scoped.apply_finder_options(options) - block_given? ? relation.extending(Module.new(&block)) : relation + scoped.apply_finder_options(options) else current_scoped_methods ? unscoped.merge(current_scoped_methods) : unscoped.clone end @@ -35,7 +37,7 @@ module ActiveRecord read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {}) end - # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query, + # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query, # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>. # # class Shirt < ActiveRecord::Base @@ -85,18 +87,22 @@ module ActiveRecord # end def scope(name, scope_options = {}, &block) name = name.to_sym + valid_scope_name?(name) - if !scopes[name] && respond_to?(name, true) - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." - end + extension = Module.new(&block) if block_given? scopes[name] = lambda do |*args| options = scope_options.is_a?(Proc) ? scope_options.call(*args) : scope_options - relation = scoped - relation = options.is_a?(Hash) ? relation.apply_finder_options(options) : scoped.merge(options) if options - block_given? ? relation.extending(Module.new(&block)) : relation + relation = if options.is_a?(Hash) + scoped.apply_finder_options(options) + elsif options + scoped.merge(options) + else + scoped + end + + extension ? relation.extending(extension) : relation end singleton_class.send :define_method, name, &scopes[name] @@ -106,7 +112,15 @@ module ActiveRecord ActiveSupport::Deprecation.warn("Base.named_scope has been deprecated, please use Base.scope instead", caller) scope(*args, &block) end - end + protected + + def valid_scope_name?(name) + if !scopes[name] && respond_to?(name, true) + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end + end + end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index eb9e792dd8..cf8c5aaf84 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -15,7 +15,7 @@ module ActiveRecord self.nested_attributes_options = {} end - # == Nested Attributes + # = Active Record Nested Attributes # # Nested attributes allow you to save attributes on associated records # through the parent. By default nested attribute updating is turned off, @@ -25,6 +25,7 @@ module ActiveRecord # # The attribute writer is named after the association, which means that # in the following example, two new methods are added to your model: + # # <tt>author_attributes=(attributes)</tt> and # <tt>pages_attributes=(attributes)</tt>. # @@ -132,7 +133,7 @@ module ActiveRecord # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' # - # Alternatively, :reject_if also accepts a symbol for using methods: + # Alternatively, :reject_if also accepts a symbol for using methods: # # class Member < ActiveRecord::Base # has_many :posts @@ -144,7 +145,7 @@ module ActiveRecord # accepts_nested_attributes_for :posts, :reject_if => :reject_posts # # def reject_posts(attributed) - # attributed['title].blank? + # attributed['title'].blank? # end # end # @@ -212,7 +213,7 @@ module ActiveRecord # that will reject a record where all the attributes are blank. # [:limit] # Allows you to specify the maximum number of the associated records that - # can be processes with the nested attributes. If the size of the + # can be processed with the nested attributes. If the size of the # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords # exception is raised. If omitted, any number associations can be processed. # Note that the :limit option is only applicable to one-to-many associations. @@ -278,7 +279,7 @@ module ActiveRecord # Assigns the given attributes to the association. # # If update_only is false and the given attributes include an <tt>:id</tt> - # that matches the existing record’s id, then the existing record will be + # that matches the existing record's id, then the existing record will be # modified. If update_only is true, a new record is only created when no # object exists. Otherwise a new record will be built. # @@ -295,7 +296,9 @@ module ActiveRecord assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) elsif attributes['id'] - raise_nested_attributes_record_not_found(association_name, attributes['id']) + existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id']) + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + self.send(association_name.to_s+'=', existing_record) elsif !reject_new_record?(association_name, attributes) method = "build_#{association_name}" @@ -365,11 +368,16 @@ module ActiveRecord unless reject_new_record?(association_name, attributes) association.build(attributes.except(*UNASSIGNABLE_KEYS)) end + + elsif existing_records.count == 0 #Existing record but not yet associated + existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id']) + association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded? + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded? assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) - else - raise_nested_attributes_record_not_found(association_name, attributes['id']) + end end end @@ -389,7 +397,7 @@ module ActiveRecord ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) end - # Determines if a new record should be build by checking for + # Determines if a new record should be built by checking for # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this # association and evaluates to +true+. def reject_new_record?(association_name, attributes) @@ -405,9 +413,5 @@ module ActiveRecord end end - def raise_nested_attributes_record_not_found(association_name, record_id) - reflection = self.class.reflect_on_association(association_name) - raise RecordNotFound, "Couldn't find #{reflection.klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" - end end end diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index 0ea7fe7365..5f80bd86df 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -1,6 +1,8 @@ require 'active_support/core_ext/class/attribute' module ActiveRecord + # = Active Record Observer + # # Observer classes respond to lifecycle callbacks to implement trigger-like # behavior outside the original class. This is a great way to reduce the # clutter that normally comes when the model class is burdened with @@ -105,8 +107,9 @@ module ActiveRecord end protected + def observed_subclasses - observed_classes.sum([]) { |klass| klass.send(:subclasses) } + observed_classes.sum([]) { |klass| klass.send(:descendants) } end def observe_callbacks? diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 10788630a5..50166c4385 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,6 +1,8 @@ module ActiveRecord + # = Active Record Persistence module Persistence - # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false. + # Returns true if this object hasn't been saved yet -- that is, a record + # for the object doesn't exist in the data store yet; otherwise, returns false. def new_record? @new_record end @@ -10,7 +12,8 @@ module ActiveRecord @destroyed end - # Returns if the record is persisted, i.e. it's not a new record and it was not destroyed. + # Returns if the record is persisted, i.e. it's not a new record and it was + # not destroyed. def persisted? !(new_record? || destroyed?) end @@ -69,8 +72,8 @@ module ActiveRecord freeze end - # Deletes the record in the database and freezes this instance to reflect that no changes should - # be made (since they can't be persisted). + # Deletes the record in the database and freezes this instance to reflect + # that no changes should be made (since they can't be persisted). def destroy if persisted? self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).delete_all @@ -80,10 +83,13 @@ module ActiveRecord freeze end - # Returns an instance of the specified +klass+ with the attributes of the current record. This is mostly useful in relation to - # single-table inheritance structures where you want a subclass to appear as the superclass. This can be used along with record - # identification in Action Pack to allow, say, <tt>Client < Company</tt> to do something like render <tt>:partial => @client.becomes(Company)</tt> - # to render that instance using the companies/company partial instead of clients/client. + # Returns an instance of the specified +klass+ with the attributes of the + # current record. This is mostly useful in relation to single-table + # inheritance structures where you want a subclass to appear as the + # superclass. This can be used along with record identification in + # Action Pack to allow, say, <tt>Client < Company</tt> to do something + # like render <tt>:partial => @client.becomes(Company)</tt> to render that + # instance using the companies/company partial instead of clients/client. # # Note: The new instance will share a link to the same attributes as the original class. So any change to the attributes in either # instance will affect the other. @@ -104,14 +110,15 @@ module ActiveRecord save(:validate => false) end - # Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will - # fail and false will be returned. + # Updates all the attributes from the passed-in Hash and saves the record. + # If the object is invalid, the saving will fail and false will be returned. def update_attributes(attributes) self.attributes = attributes save end - # Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid. + # Updates an object just like Base.update_attributes but calls save! instead + # of save so an exception is raised if the record is invalid. def update_attributes!(attributes) self.attributes = attributes save! @@ -175,7 +182,7 @@ module ActiveRecord def reload(options = nil) clear_aggregation_cache clear_association_cache - @attributes.update(self.class.send(:with_exclusive_scope) { self.class.find(self.id, options) }.instance_variable_get('@attributes')) + @attributes.update(self.class.unscoped { self.class.find(self.id, options) }.instance_variable_get('@attributes')) @attributes_cache = {} self end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 093c6c1e55..d9f85a4e5e 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/object/blank' module ActiveRecord + # = Active Record Query Cache class QueryCache module ClassMethods # Enable the query cache within the block if Active Record is configured. diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index a32fb7d399..2808e199fe 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -9,6 +9,7 @@ require "active_model/railtie" require "action_controller/railtie" module ActiveRecord + # = Active Record Railtie class Railtie < Rails::Railtie config.active_record = ActiveSupport::OrderedOptions.new @@ -25,9 +26,6 @@ module ActiveRecord load "active_record/railties/databases.rake" end - require "active_record/railties/log_subscriber" - log_subscriber :active_record, ActiveRecord::Railties::LogSubscriber.new - initializer "active_record.initialize_timezone" do ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true @@ -68,7 +66,6 @@ module ActiveRecord unless app.config.cache_classes ActiveSupport.on_load(:active_record) do ActionDispatch::Callbacks.after do - ActiveRecord::Base.reset_subclasses ActiveRecord::Base.clear_reloadable_connections! end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 006e64b115..7882f05d07 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -57,7 +57,7 @@ namespace :db do end rescue case config['adapter'] - when 'mysql' + when /mysql/ @charset = ENV['CHARSET'] || 'utf8' @collation = ENV['COLLATION'] || 'utf8_unicode_ci' creation_options = {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)} @@ -171,6 +171,36 @@ namespace :db do ActiveRecord::Migrator.run(:down, "db/migrate/", version) Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby end + + desc "Display status of migrations" + task :status => :environment do + config = ActiveRecord::Base.configurations[Rails.env || 'development'] + ActiveRecord::Base.establish_connection(config) + unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) + puts 'Schema migrations table does not exist yet.' + next # means "return" for rake task + end + db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") + file_list = [] + Dir.foreach(File.join(Rails.root, 'db', 'migrate')) do |file| + # only files matching "20091231235959_some_name.rb" pattern + if match_data = /(\d{14})_(.+)\.rb/.match(file) + status = db_list.delete(match_data[1]) ? 'up' : 'down' + file_list << [status, match_data[1], match_data[2]] + end + end + # output + puts "\ndatabase: #{config['database']}\n\n" + puts "#{"Status".center(8)} #{"Migration ID".ljust(14)} Migration Name" + puts "-" * 50 + file_list.each do |file| + puts "#{file[0].center(8)} #{file[1].ljust(14)} #{file[2].humanize}" + end + db_list.each do |version| + puts "#{'up'.center(8)} #{version.ljust(14)} *** NO FILE ***" + end + puts + end end desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).' @@ -194,7 +224,7 @@ namespace :db do task :charset => :environment do config = ActiveRecord::Base.configurations[Rails.env || 'development'] case config['adapter'] - when 'mysql' + when /mysql/ ActiveRecord::Base.establish_connection(config) puts ActiveRecord::Base.connection.charset when 'postgresql' @@ -212,7 +242,7 @@ namespace :db do task :collation => :environment do config = ActiveRecord::Base.configurations[Rails.env || 'development'] case config['adapter'] - when 'mysql' + when /mysql/ ActiveRecord::Base.establish_connection(config) puts ActiveRecord::Base.connection.collation else @@ -313,7 +343,7 @@ namespace :db do task :dump => :environment do abcs = ActiveRecord::Base.configurations case abcs[Rails.env]["adapter"] - when "mysql", "oci", "oracle" + when /mysql/, "oci", "oracle" ActiveRecord::Base.establish_connection(abcs[Rails.env]) File.open("#{Rails.root}/db/#{Rails.env}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } when "postgresql" @@ -361,7 +391,7 @@ namespace :db do task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do abcs = ActiveRecord::Base.configurations case abcs["test"]["adapter"] - when "mysql" + when /mysql/ ActiveRecord::Base.establish_connection(:test) ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') IO.readlines("#{Rails.root}/db/#{Rails.env}_structure.sql").join.split("\n\n").each do |table| @@ -395,7 +425,7 @@ namespace :db do task :purge => :environment do abcs = ActiveRecord::Base.configurations case abcs["test"]["adapter"] - when "mysql" + when /mysql/ ActiveRecord::Base.establish_connection(:test) ActiveRecord::Base.connection.recreate_database(abcs["test"]["database"], abcs["test"]) when "postgresql" @@ -451,7 +481,7 @@ task 'test:prepare' => 'db:test:prepare' def drop_database(config) case config['adapter'] - when 'mysql' + when /mysql/ ActiveRecord::Base.establish_connection(config) ActiveRecord::Base.connection.drop_database config['database'] when /^sqlite/ diff --git a/activerecord/lib/active_record/railties/log_subscriber.rb b/activerecord/lib/active_record/railties/log_subscriber.rb deleted file mode 100644 index 31b98bb6ed..0000000000 --- a/activerecord/lib/active_record/railties/log_subscriber.rb +++ /dev/null @@ -1,32 +0,0 @@ -module ActiveRecord - module Railties - class LogSubscriber < Rails::LogSubscriber - def initialize - super - @odd_or_even = false - end - - def sql(event) - name = '%s (%.1fms)' % [event.payload[:name], event.duration] - sql = event.payload[:sql].squeeze(' ') - - if odd? - name = color(name, :cyan, true) - sql = color(sql, nil, true) - else - name = color(name, :magenta, true) - end - - debug " #{name} #{sql}" - end - - def odd? - @odd_or_even = !@odd_or_even - end - - def logger - ActiveRecord::Base.logger - end - end - end -end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0e48e229b3..a82e5d7ed1 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,12 +1,16 @@ module ActiveRecord + # = Active Record Reflection module Reflection # :nodoc: extend ActiveSupport::Concern - # Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations. - # This information can, for example, be used in a form builder that took an Active Record object and created input - # fields for all of the attributes depending on their type and displayed the associations to other objects. + # Reflection allows you to interrogate Active Record classes and objects + # about their associations and aggregations. This information can, + # for example, be used in a form builder that took an Active Record object + # and created input fields for all of the attributes depending on their type + # and displayed the associations to other objects. # - # You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class. + # You can find the interface for the AggregateReflection and AssociationReflection + # classes in the abstract MacroReflection class. module ClassMethods def create_reflection(macro, name, options, active_record) case macro @@ -43,8 +47,11 @@ module ActiveRecord reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil end - # Returns an array of AssociationReflection objects for all the associations in the class. If you only want to reflect on a - # certain association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, <tt>:belongs_to</tt>) for that as the first parameter. + # Returns an array of AssociationReflection objects for all the + # associations in the class. If you only want to reflect on a certain + # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, + # <tt>:belongs_to</tt>) for that as the first parameter. + # # Example: # # Account.reflect_on_all_associations # returns an array of all associations @@ -71,8 +78,9 @@ module ActiveRecord end - # Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of - # those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. + # Abstract base class for AggregateReflection and AssociationReflection that + # describes the interface available for both of those classes. Objects of + # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. class MacroReflection attr_reader :active_record @@ -80,13 +88,15 @@ module ActiveRecord @macro, @name, @options, @active_record = macro, name, options, active_record end - # Returns the name of the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> will return - # <tt>:balance</tt> or for <tt>has_many :clients</tt> it will return <tt>:clients</tt>. + # Returns the name of the macro. For example, <tt>composed_of :balance, + # :class_name => 'Money'</tt> will return <tt>:balance</tt> or for + # <tt>has_many :clients</tt> it will return <tt>:clients</tt>. def name @name end - # Returns the macro type. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> will return <tt>:composed_of</tt> + # Returns the macro type. For example, + # <tt>composed_of :balance, :class_name => 'Money'</tt> will return <tt>:composed_of</tt> # or for <tt>has_many :clients</tt> will return <tt>:has_many</tt>. def macro @macro @@ -132,11 +142,13 @@ module ActiveRecord end - # Holds all the meta-data about an aggregation as it was specified in the Active Record class. + # Holds all the meta-data about an aggregation as it was specified in the + # Active Record class. class AggregateReflection < MacroReflection #:nodoc: end - # Holds all the meta-data about an association as it was specified in the Active Record class. + # Holds all the meta-data about an association as it was specified in the + # Active Record class. class AssociationReflection < MacroReflection #:nodoc: # Returns the target association's class: # @@ -165,14 +177,14 @@ module ActiveRecord klass.new(*options) end - # Creates a new instance of the associated class, and immediates saves it + # Creates a new instance of the associated class, and immediately saves it # with ActiveRecord::Base#save. +options+ will be passed to the class's # creation method. Returns the newly created object. def create_association(*options) klass.create(*options) end - # Creates a new instance of the associated class, and immediates saves it + # Creates a new instance of the associated class, and immediately saves it # with ActiveRecord::Base#save!. +options+ will be passed to the class's # creation method. If the created record doesn't pass validations, then an # exception will be raised. @@ -267,10 +279,10 @@ module ActiveRecord # Returns whether or not the association should be validated as part of # the parent's validation. # - # Unless you explicitely disable validation with + # Unless you explicitly disable validation with # <tt>:validate => false</tt>, it will take place when: # - # * you explicitely enable validation; <tt>:validate => true</tt> + # * you explicitly enable validation; <tt>:validate => true</tt> # * you use autosave; <tt>:autosave => true</tt> # * the association is a +has_many+ association def validate? @@ -306,9 +318,12 @@ module ActiveRecord end end - # Holds all the meta-data about a :through association as it was specified in the Active Record class. + # Holds all the meta-data about a :through association as it was specified + # in the Active Record class. class ThroughReflection < AssociationReflection #:nodoc: - # Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. + # Gets the source of the through reflection. It checks both a singularized + # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. + # # (The <tt>:tags</tt> association on Tagging below.) # # class Post < ActiveRecord::Base diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 99c914d7fc..bc708b573f 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/object/blank' module ActiveRecord + # = Active Record Relation class Relation JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] @@ -9,13 +10,13 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches - delegate :length, :collect, :map, :each, :all?, :include?, :to => :to_a + delegate :to_xml, :to_json, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a delegate :insert, :to => :arel attr_reader :table, :klass attr_accessor :extensions - def initialize(klass, table, &block) + def initialize(klass, table) @klass, @table = klass, table @implicit_readonly = nil @@ -24,12 +25,10 @@ module ActiveRecord SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)} (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} @extensions = [] - - apply_modules(Module.new(&block)) if block_given? end def new(*args, &block) - with_create_scope { @klass.new(*args, &block) } + scoping { @klass.new(*args, &block) } end def initialize_copy(other) @@ -39,11 +38,11 @@ module ActiveRecord alias build new def create(*args, &block) - with_create_scope { @klass.create(*args, &block) } + scoping { @klass.create(*args, &block) } end def create!(*args, &block) - with_create_scope { @klass.create!(*args, &block) } + scoping { @klass.create!(*args, &block) } end def respond_to?(method, include_private = false) @@ -67,7 +66,7 @@ module ActiveRecord preload += @includes_values unless eager_loading? preload.each {|associations| @klass.send(:preload_associations, @records, associations) } - # @readonly_value is true only if set explicity. @implicit_readonly is true if there are JOINS and no explicit SELECT. + # @readonly_value is true only if set explicitly. @implicit_readonly is true if there are JOINS and no explicit SELECT. readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value @records.each { |record| record.readonly! } if readonly @@ -75,10 +74,12 @@ module ActiveRecord @records end + # Returns size of the records. def size loaded? ? @records.length : count end + # Returns true if there are no records. def empty? loaded? ? @records.empty? : count.zero? end @@ -99,6 +100,25 @@ module ActiveRecord end end + # Scope all queries to the current scope. + # + # ==== Example + # + # Comment.where(:post_id => 1).scoping do + # Comment.first #=> SELECT * FROM comments WHERE post_id = 1 + # end + # + # Please check unscoped if you want to remove all previous scopes (including + # the default_scope) during the execution of a block. + def scoping + @klass.scoped_methods << self + begin + yield + ensure + @klass.scoped_methods.pop + end + end + # Updates all records with details given if they match a set of conditions supplied, limits and order can # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the # database. It does not instantiate the involved models and it does not trigger Active Record callbacks @@ -240,8 +260,9 @@ module ActiveRecord # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) # - # Both calls delete the affected posts all at once with a single DELETE statement. If you need to destroy dependent - # associations or call your <tt>before_*</tt> or +after_destroy+ callbacks, use the +destroy_all+ method instead. + # Both calls delete the affected posts all at once with a single DELETE statement. + # If you need to destroy dependent associations or call your <tt>before_*</tt> or + # +after_destroy+ callbacks, use the +destroy_all+ method instead. def delete_all(conditions = nil) conditions ? where(conditions).delete_all : arel.delete.tap { reset } end @@ -301,7 +322,6 @@ module ActiveRecord if where.is_a?(Arel::Predicates::Equality) hash[where.operand1.name] = where.operand2.respond_to?(:value) ? where.operand2.value : where.operand2 end - hash end end @@ -332,7 +352,7 @@ module ActiveRecord elsif @klass.scopes[method] merge(@klass.send(method, *args, &block)) elsif @klass.respond_to?(method) - @klass.send(:with_scope, self) { @klass.send(method, *args, &block) } + scoping { @klass.send(method, *args, &block) } elsif arel.respond_to?(method) arel.send(method, *args, &block) elsif match = DynamicFinderMatch.match(method) @@ -351,10 +371,6 @@ module ActiveRecord private - def with_create_scope - @klass.send(:with_scope, :create => scope_for_create, :find => {}) { yield } - end - def references_eager_loaded_tables? # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.map(&:downcase).uniq diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 7a0c9dc612..f39951e16c 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -87,8 +87,8 @@ module ActiveRecord # person.visits += 1 # person.save! # end - def find(*args, &block) - return to_a.find(&block) if block_given? + def find(*args) + return to_a.find { |*block_args| yield(*block_args) } if block_given? options = args.extract_options! @@ -259,8 +259,8 @@ module ActiveRecord record end - def find_with_ids(*ids, &block) - return to_a.find(&block) if block_given? + def find_with_ids(*ids) + return to_a.find { |*block_args| yield(*block_args) } if block_given? expects_array = ids.first.kind_of?(Array) return ids.first if expects_array && ids.first.empty? diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index d0efa2189d..5cea2328e8 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -20,15 +20,13 @@ module ActiveRecord table = Arel::Table.new(table_name, :engine => @engine) end - unless attribute = table[column] - raise StatementInvalid, "No attribute named `#{column}` exists for table `#{table.name}`" - end + attribute = table[column] || Arel::Attribute.new(table, column) case value when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation values = value.to_a attribute.in(values) - when Range + when Range, Arel::Relation attribute.in(value) else attribute.eq(value) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 7a48a6596a..4692271266 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -5,79 +5,92 @@ module ActiveRecord module QueryMethods extend ActiveSupport::Concern - included do - (ActiveRecord::Relation::ASSOCIATION_METHODS + ActiveRecord::Relation::MULTI_VALUE_METHODS).each do |query_method| - attr_accessor :"#{query_method}_values" - - next if [:where, :having, :select].include?(query_method) - class_eval <<-CEVAL, __FILE__, __LINE__ + 1 - def #{query_method}(*args, &block) - new_relation = clone - new_relation.send(:apply_modules, Module.new(&block)) if block_given? - value = Array.wrap(args.flatten).reject {|x| x.blank? } - new_relation.#{query_method}_values += value if value.present? - new_relation - end - CEVAL - end + attr_accessor :includes_values, :eager_load_values, :preload_values, + :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, + :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value - class_eval <<-CEVAL, __FILE__, __LINE__ + 1 - def select(*args, &block) - if block_given? - to_a.select(&block) - else - new_relation = clone - value = Array.wrap(args.flatten).reject {|x| x.blank? } - new_relation.select_values += value if value.present? - new_relation - end - end - CEVAL - - [:where, :having].each do |query_method| - class_eval <<-CEVAL, __FILE__, __LINE__ + 1 - def #{query_method}(*args, &block) - new_relation = clone - new_relation.send(:apply_modules, Module.new(&block)) if block_given? - value = build_where(*args) - new_relation.#{query_method}_values += Array.wrap(value) if value.present? - new_relation - end - CEVAL - end + def includes(*args) + args.reject! { |a| a.blank? } + clone.tap { |r| r.includes_values += args if args.present? } + end + + def eager_load(*args) + clone.tap { |r| r.eager_load_values += args if args.present? } + end - ActiveRecord::Relation::SINGLE_VALUE_METHODS.each do |query_method| - attr_accessor :"#{query_method}_value" + def preload(*args) + clone.tap { |r| r.preload_values += args if args.present? } + end - class_eval <<-CEVAL, __FILE__, __LINE__ + 1 - def #{query_method}(value = true, &block) - new_relation = clone - new_relation.send(:apply_modules, Module.new(&block)) if block_given? - new_relation.#{query_method}_value = value - new_relation - end - CEVAL + def select(*args) + if block_given? + to_a.select { |*block_args| yield(*block_args) } + else + clone.tap { |r| r.select_values += args if args.present? } end end - def extending(*modules) - new_relation = clone - new_relation.send :apply_modules, *modules - new_relation + def group(*args) + clone.tap { |r| r.group_values += args if args.present? } end - def lock(locks = true, &block) - relation = clone - relation.send(:apply_modules, Module.new(&block)) if block_given? + def order(*args) + clone.tap { |r| r.order_values += args if args.present? } + end + + def reorder(*args) + clone.tap { |r| r.order_values = args if args.present? } + end + def joins(*args) + args.flatten! + clone.tap { |r| r.joins_values += args if args.present? } + end + + def where(*args) + value = build_where(*args) + clone.tap { |r| r.where_values += Array.wrap(value) if value.present? } + end + + def having(*args) + value = build_where(*args) + clone.tap { |r| r.having_values += Array.wrap(value) if value.present? } + end + + def limit(value = true) + clone.tap { |r| r.limit_value = value } + end + + def offset(value = true) + clone.tap { |r| r.offset_value = value } + end + + def lock(locks = true) case locks when String, TrueClass, NilClass - clone.tap {|new_relation| new_relation.lock_value = locks || true } + clone.tap { |r| r.lock_value = locks || true } else - clone.tap {|new_relation| new_relation.lock_value = false } + clone.tap { |r| r.lock_value = false } end end + def readonly(value = true) + clone.tap { |r| r.readonly_value = value } + end + + def create_with(value = true) + clone.tap { |r| r.create_with_value = value } + end + + def from(value = true) + clone.tap { |r| r.from_value = value } + end + + def extending(*modules, &block) + modules << Module.new(&block) if block_given? + clone.tap { |r| r.send(:apply_modules, *modules) } + end + def reverse_order order_clause = arel.send(:order_clauses).join(', ') relation = except(:order) @@ -116,45 +129,7 @@ module ActiveRecord def build_arel arel = table - joined_associations = [] - association_joins = [] - - joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq - - joins.each do |join| - association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join) - end - - stashed_association_joins = joins.select {|j| j.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)} - - non_association_joins = (joins - association_joins - stashed_association_joins).reject {|j| j.blank?} - custom_joins = custom_join_sql(*non_association_joins) - - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins) - - join_dependency.graft(*stashed_association_joins) - - @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? - - to_join = [] - - join_dependency.join_associations.each do |association| - if (association_relation = association.relation).is_a?(Array) - to_join << [association_relation.first, association.join_class, association.association_join.first] - to_join << [association_relation.last, association.join_class, association.association_join.last] - else - to_join << [association_relation, association.join_class, association.association_join] - end - end - - to_join.each do |tj| - unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] } - joined_associations << tj - arel = arel.join(tj[0], tj[1]).on(*tj[2]) - end - end - - arel = arel.join(custom_joins) + arel = build_joins(arel, @joins_values) if @joins_values.present? @where_values.uniq.each do |where| next if where.blank? @@ -168,31 +143,18 @@ module ActiveRecord end end - @having_values.uniq.each do |h| - arel = h.is_a?(String) ? arel.having(h) : arel.having(*h) - end + arel = arel.having(*@having_values.uniq.select{|h| h.present?}) if @having_values.present? arel = arel.take(@limit_value) if @limit_value.present? arel = arel.skip(@offset_value) if @offset_value.present? - arel = arel.group(*@group_values.uniq.select{|g| g.present?}) - - arel = arel.order(*@order_values.uniq.select{|o| o.present?}.map(&:to_s)) + arel = arel.group(*@group_values.uniq.select{|g| g.present?}) if @group_values.present? - selects = @select_values.uniq + arel = arel.order(*@order_values.uniq.select{|o| o.present?}) if @order_values.present? - quoted_table_name = @klass.quoted_table_name - - if selects.present? - selects.each do |s| - @implicit_readonly = false - arel = arel.project(s) if s.present? - end - else - arel = arel.project(quoted_table_name + '.*') - end + arel = build_select(arel, @select_values.uniq) - arel = @from_value.present? ? arel.from(@from_value) : arel.from(quoted_table_name) + arel = arel.from(@from_value) if @from_value.present? case @lock_value when TrueClass @@ -221,6 +183,63 @@ module ActiveRecord private + def build_joins(relation, joins) + joined_associations = [] + association_joins = [] + + joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq + + joins.each do |join| + association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join) + end + + stashed_association_joins = joins.select {|j| j.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)} + + non_association_joins = (joins - association_joins - stashed_association_joins) + custom_joins = custom_join_sql(*non_association_joins) + + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins) + + join_dependency.graft(*stashed_association_joins) + + @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? + + to_join = [] + + join_dependency.join_associations.each do |association| + if (association_relation = association.relation).is_a?(Array) + to_join << [association_relation.first, association.join_class, association.association_join.first] + to_join << [association_relation.last, association.join_class, association.association_join.last] + else + to_join << [association_relation, association.join_class, association.association_join] + end + end + + to_join.each do |tj| + unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] } + joined_associations << tj + relation = relation.join(tj[0], tj[1]).on(*tj[2]) + end + end + + relation.join(custom_joins) + end + + def build_select(arel, selects) + if selects.present? + @implicit_readonly = false + # TODO: fix this ugly hack, we should refactor the callers to get an ARel compatible array. + # Before this change we were passing to ARel the last element only, and ARel is capable of handling an array + if selects.all? { |s| s.is_a?(String) || !s.is_a?(Arel::Expression) } && !(selects.last =~ /^COUNT\(/) + arel.project(*selects) + else + arel.project(selects.last) + end + else + arel.project(@klass.quoted_table_name + '.*') + end + end + def apply_modules(modules) values = Array.wrap(modules) @extensions += values if values.present? diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index bb1f138f5b..7712ad2569 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -6,7 +6,7 @@ module ActiveRecord merged_relation = clone return merged_relation unless r - (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).reject {|m| [:joins, :where].include?(m)}.each do |method| + ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - [:joins, :where]).each do |method| value = r.send(:"#{method}_values") merged_relation.send(:"#{method}_values=", value) if value.present? end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index a833356d15..e2783087ec 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,6 +1,8 @@ require 'active_support/core_ext/object/blank' module ActiveRecord + # = Active Record Schema + # # Allows programmers to programmatically define a schema in a portable # DSL. This means you can define tables, indexes, etc. without using SQL # directly, so your applications can more easily support multiple diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index cd54653581..a4757773d8 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -2,6 +2,8 @@ require 'stringio' require 'active_support/core_ext/big_decimal' module ActiveRecord + # = Active Record Schema Dumper + # # This class is used to dump the database schema for some connection to some # output format (i.e., ActiveRecord::Schema). class SchemaDumper #:nodoc: @@ -39,13 +41,14 @@ module ActiveRecord define_params = @version ? ":version => #{@version}" : "" stream.puts <<HEADER -# This file is auto-generated from the current state of the database. Instead of editing this file, -# please use the migrations feature of Active Record to incrementally modify your database, and -# then regenerate this schema definition. +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your database schema. If you need -# to create the application database on another system, you should be using db:schema:load, not running -# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # # It's strongly recommended to check this file into your version control system. @@ -173,15 +176,15 @@ HEADER def indexes(table, stream) if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| - statment_parts = [ ('add_index ' + index.table.inspect) ] - statment_parts << index.columns.inspect - statment_parts << (':name => ' + index.name.inspect) - statment_parts << ':unique => true' if index.unique + statement_parts = [ ('add_index ' + index.table.inspect) ] + statement_parts << index.columns.inspect + statement_parts << (':name => ' + index.name.inspect) + statement_parts << ':unique => true' if index.unique index_lengths = index.lengths.compact if index.lengths.is_a?(Array) - statment_parts << (':length => ' + Hash[*index.columns.zip(index.lengths).flatten].inspect) if index_lengths.present? + statement_parts << (':length => ' + Hash[*index.columns.zip(index.lengths).flatten].inspect) if index_lengths.present? - ' ' + statment_parts.join(', ') + ' ' + statement_parts.join(', ') end stream.puts add_index_statements.sort.join("\n") diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index b49471f7ab..2d8bd184f3 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,4 +1,5 @@ module ActiveRecord #:nodoc: + # = Active Record Serialization module Serialization extend ActiveSupport::Concern include ActiveModel::Serializers::JSON @@ -22,6 +23,7 @@ module ActiveRecord #:nodoc: private # Add associations specified via the <tt>:includes</tt> option. + # # Expects a block that takes as arguments: # +association+ - name of the association # +records+ - the association record(s) to be serialized diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index 931872eded..b88d550086 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -1,4 +1,6 @@ module ActiveRecord + # = Active Record Session Store + # # A session store backed by an Active Record class. A default class is # provided, but any object duck-typing to an Active Record Session class # with text +session_id+ and +data+ attributes is sufficient. @@ -7,6 +9,7 @@ module ActiveRecord # +id+ (numeric primary key), # +session_id+ (text, or longtext if your session data exceeds 65K), and # +data+ (text or longtext; careful if your session data exceeds 65KB). + # # The +session_id+ column should always be indexed for speedy lookups. # Session data is marshaled to the +data+ column in Base64 format. # If the data you write is larger than the column's size limit, @@ -14,9 +17,11 @@ module ActiveRecord # # You may configure the table name, primary key, and data column. # For example, at the end of <tt>config/environment.rb</tt>: + # # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table' # ActiveRecord::SessionStore::Session.primary_key = 'session_id' # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data' + # # Note that setting the primary key to the +session_id+ frees you from # having a separate +id+ column if you don't want it. However, you must # set <tt>session.model.id = session.session_id</tt> by hand! A before filter @@ -29,8 +34,11 @@ module ActiveRecord # You may provide your own session class implementation, whether a # feature-packed Active Record or a bare-metal high-performance SQL # store, by setting + # # ActiveRecord::SessionStore.session_class = MySessionClass + # # You must implement these methods: + # # self.find_by_session_id(session_id) # initialize(hash_of_session_id_and_data) # attr_reader :session_id @@ -310,6 +318,14 @@ module ActiveRecord sid end + def destroy(env) + if sid = current_session_id(env) + Base.silence do + get_session_model(env, sid).destroy + end + end + end + def get_session_model(env, sid) if env[ENV_SESSION_OPTIONS_KEY][:id].nil? env[SESSION_RECORD_KEY] = find_session(sid) diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 0a77ad5fd7..e61a378d17 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -1,4 +1,7 @@ module ActiveRecord + # = Active Record Test Case + # + # Defines some test assertions to test against SQL queries. class TestCase < ActiveSupport::TestCase #:nodoc: def assert_date_from_db(expected, actual, message = nil) # SybaseAdapter doesn't have a separate column type just for dates, diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 9fba8f0aca..ffd12d2082 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,11 +1,16 @@ module ActiveRecord - # Active Record automatically timestamps create and update operations if the table has fields - # named created_at/created_on or updated_at/updated_on. + # = Active Record Timestamp + # + # Active Record automatically timestamps create and update operations if the + # table has fields named <tt>created_at/created_on</tt> or + # <tt>updated_at/updated_on</tt>. + # + # Timestamping can be turned off by setting: # - # Timestamping can be turned off by setting # <tt>ActiveRecord::Base.record_timestamps = false</tt> # - # Timestamps are in the local timezone by default but you can use UTC by setting + # Timestamps are in the local timezone by default but you can use UTC by setting: + # # <tt>ActiveRecord::Base.default_timezone = :utc</tt> module Timestamp extend ActiveSupport::Concern @@ -16,8 +21,9 @@ module ActiveRecord end # Saves the record with the updated_at/on attributes set to the current time. - # If the save fails because of validation errors, an ActiveRecord::RecordInvalid exception is raised. - # If an attribute name is passed, that attribute is used for the touch instead of the updated_at/on attributes. + # If the save fails because of validation errors, an + # ActiveRecord::RecordInvalid exception is raised. If an attribute name is passed, + # that attribute is used for the touch instead of the updated_at/on attributes. # # Examples: # diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 620758f5af..a7709b9489 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -11,7 +11,8 @@ module ActiveRecord included do define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name] end - + # = Active Record Transactions + # # Transactions are protective blocks where SQL statements are only permanent # if they can all succeed as one atomic action. The classic example is a # transfer between two accounts where you can only have a deposit if the @@ -19,7 +20,8 @@ module ActiveRecord # the database and guard the data against program errors or database # break-downs. So basically you should use transaction blocks whenever you # have a number of statements that must be executed together or not at all. - # Example: + # + # For example: # # ActiveRecord::Base.transaction do # david.withdrawal(100) @@ -320,6 +322,7 @@ module ActiveRecord if @_start_transaction_state[:level] < 1 restore_state = remove_instance_variable(:@_start_transaction_state) if restore_state + @attributes = @attributes.dup if @attributes.frozen? @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] if restore_state[:id] diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index be64e00bd1..b98fd353aa 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -1,6 +1,9 @@ module ActiveRecord + # = Active Record Validations + # # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the # +record+ method to retrieve the record which did not validate. + # # begin # complex_operation_that_calls_save!_internally # rescue ActiveRecord::RecordInvalid => invalid @@ -49,12 +52,12 @@ module ActiveRecord # Runs all the specified validations and returns true if no errors were added otherwise false. def valid?(context = nil) context ||= (new_record? ? :create : :update) - super(context) + output = super(context) deprecated_callback_method(:validate) deprecated_callback_method(:"validate_on_#{context}") - errors.empty? + errors.empty? && output end protected diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index e41635134c..0b0f5682aa 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -3,7 +3,7 @@ module ActiveRecord class AssociatedValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all? - record.errors.add(attribute, :invalid, :default => options[:message], :value => value) + record.errors.add(attribute, :invalid, options.merge(:value => value)) end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 6283bdd0d6..1c9ecc7b1b 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -32,7 +32,7 @@ module ActiveRecord end if relation.exists? - record.errors.add(attribute, :taken, :default => options[:message], :value => value) + record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value)) end end diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index bbb7c53d86..8ac21c1410 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -1,11 +1,17 @@ class <%= migration_class_name %> < ActiveRecord::Migration - def self.up<% attributes.each do |attribute| %> - <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end -%> - <%- end %> + def self.up +<% attributes.each do |attribute| -%> + <%- if migration_action -%> + <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end %> + <%- end -%> +<%- end -%> end - def self.down<% attributes.reverse.each do |attribute| %> - <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end -%> - <%- end %> + def self.down +<% attributes.reverse.each do |attribute| -%> + <%- if migration_action -%> + <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end %> + <%- end -%> +<%- end -%> end end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 539c2517ee..960c29c49c 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -22,7 +22,7 @@ module ActiveRecord def create_module_file return if class_path.empty? - template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") + template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke end hook_for :test_framework diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index fc08c2178a..646aa88d80 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -65,15 +65,14 @@ class AdapterTest < ActiveRecord::TestCase end def test_not_specifying_database_name_for_cross_database_selects - assert_nothing_raised do - ActiveRecord::Base.establish_connection({ - :adapter => 'mysql', - :username => 'rails' - }) - ActiveRecord::Base.connection.execute "SELECT activerecord_unittest.pirates.*, activerecord_unittest2.courses.* FROM activerecord_unittest.pirates, activerecord_unittest2.courses" + begin + assert_nothing_raised do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database)) + ActiveRecord::Base.connection.execute "SELECT activerecord_unittest.pirates.*, activerecord_unittest2.courses.* FROM activerecord_unittest.pirates, activerecord_unittest2.courses" + end + ensure + ActiveRecord::Base.establish_connection 'arunit' end - - ActiveRecord::Base.establish_connection 'arunit' end end diff --git a/activerecord/test/cases/connection_test_firebird.rb b/activerecord/test/cases/adapters/firebird/connection_test.rb index f57ea686a5..f57ea686a5 100644 --- a/activerecord/test/cases/connection_test_firebird.rb +++ b/activerecord/test/cases/adapters/firebird/connection_test.rb diff --git a/activerecord/test/cases/default_test_firebird.rb b/activerecord/test/cases/adapters/firebird/default_test.rb index 713c7e11bf..713c7e11bf 100644 --- a/activerecord/test/cases/default_test_firebird.rb +++ b/activerecord/test/cases/adapters/firebird/default_test.rb diff --git a/activerecord/test/cases/migration_test_firebird.rb b/activerecord/test/cases/adapters/firebird/migration_test.rb index 710661b9bd..710661b9bd 100644 --- a/activerecord/test/cases/migration_test_firebird.rb +++ b/activerecord/test/cases/adapters/firebird/migration_test.rb diff --git a/activerecord/test/cases/active_schema_test_mysql.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index d7431e5158..6e6645511c 100644 --- a/activerecord/test/cases/active_schema_test_mysql.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -17,8 +17,8 @@ class ActiveSchemaTest < ActiveRecord::TestCase end def test_add_index - # add_index calls index_exists? which can't work since execute is stubbed - ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:define_method, :index_exists?) do |*| + # add_index calls index_name_exists? which can't work since execute is stubbed + ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:define_method, :index_name_exists?) do |*| false end expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)" @@ -35,7 +35,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10))" assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10}) - ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:remove_method, :index_exists?) + ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:remove_method, :index_name_exists?) end def test_drop_table diff --git a/activerecord/test/cases/connection_test_mysql.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 8e4842a1b6..8e4842a1b6 100644 --- a/activerecord/test/cases/connection_test_mysql.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb diff --git a/activerecord/test/cases/reserved_word_test_mysql.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 90d8b0d923..90d8b0d923 100644 --- a/activerecord/test/cases/reserved_word_test_mysql.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb diff --git a/activerecord/test/cases/synonym_test_oracle.rb b/activerecord/test/cases/adapters/oracle/synonym_test.rb index b9a422a6ca..b9a422a6ca 100644 --- a/activerecord/test/cases/synonym_test_oracle.rb +++ b/activerecord/test/cases/adapters/oracle/synonym_test.rb diff --git a/activerecord/test/cases/active_schema_test_postgresql.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 4f04c6735c..f106e14319 100644 --- a/activerecord/test/cases/active_schema_test_postgresql.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -10,7 +10,10 @@ class PostgresqlActiveSchemaTest < Test::Unit::TestCase end def teardown - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:alias_method, :execute, :real_execute) + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do + remove_method :execute + alias_method :execute, :real_execute + end end def test_create_database_with_encoding diff --git a/activerecord/test/cases/datatype_test_postgresql.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 3c2d9fb7bd..5bb8fa2f93 100644 --- a/activerecord/test/cases/datatype_test_postgresql.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -53,7 +53,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase @connection.execute("INSERT INTO postgresql_oids (obj_id) VALUES (1234)") @first_oid = PostgresqlOid.find(1) - + @connection.execute("INSERT INTO postgresql_timestamp_with_zones (time) VALUES ('2010-01-01 10:00:00-1')") end diff --git a/activerecord/test/cases/schema_authorization_test_postgresql.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 2860f1ad48..6f372edc38 100644 --- a/activerecord/test/cases/schema_authorization_test_postgresql.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -66,7 +66,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase end end end - + def test_tables_in_current_schemas assert !@connection.tables.include?(TABLE_NAME) USERS.each do |u| diff --git a/activerecord/test/cases/schema_test_postgresql.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 3ed9b1974d..a5c3e69af9 100644 --- a/activerecord/test/cases/schema_test_postgresql.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -9,9 +9,11 @@ class SchemaTest < ActiveRecord::TestCase CAPITALIZED_TABLE_NAME = 'Things' INDEX_A_NAME = 'a_index_things_on_name' INDEX_B_NAME = 'b_index_things_on_different_columns_in_each_schema' + INDEX_C_NAME = 'c_index_full_text_search' INDEX_A_COLUMN = 'name' INDEX_B_COLUMN_S1 = 'email' INDEX_B_COLUMN_S2 = 'moment' + INDEX_C_COLUMN = %q{(to_tsvector('english', coalesce(things.name, '')))} COLUMNS = [ 'id integer', 'name character varying(50)', @@ -45,6 +47,8 @@ class SchemaTest < ActiveRecord::TestCase @connection.execute "CREATE INDEX #{INDEX_A_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_A_COLUMN});" @connection.execute "CREATE INDEX #{INDEX_B_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_B_COLUMN_S1});" @connection.execute "CREATE INDEX #{INDEX_B_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_B_COLUMN_S2});" + @connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});" + @connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});" end def teardown diff --git a/activerecord/test/cases/copy_table_test_sqlite.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 575b4806c1..575b4806c1 100644 --- a/activerecord/test/cases/copy_table_test_sqlite.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb new file mode 100644 index 0000000000..934cf72f72 --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -0,0 +1,56 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class SQLite3AdapterTest < ActiveRecord::TestCase + def test_connection_no_db + assert_raises(ArgumentError) do + Base.sqlite3_connection {} + end + end + + def test_connection_no_adapter + assert_raises(ArgumentError) do + Base.sqlite3_connection :database => ':memory:' + end + end + + def test_connection_wrong_adapter + assert_raises(ArgumentError) do + Base.sqlite3_connection :database => ':memory:',:adapter => 'vuvuzela' + end + end + + def test_bad_timeout + assert_raises(TypeError) do + Base.sqlite3_connection :database => ':memory:', + :adapter => 'sqlite3', + :timeout => 'usa' + end + end + + # connection is OK with a nil timeout + def test_nil_timeout + conn = Base.sqlite3_connection :database => ':memory:', + :adapter => 'sqlite3', + :timeout => nil + assert conn, 'made a connection' + end + + def test_connect + conn = Base.sqlite3_connection :database => ':memory:', + :adapter => 'sqlite3', + :timeout => 100 + assert conn, 'should have connection' + end + + # sqlite3 defaults to UTF-8 encoding + def test_encoding + conn = Base.sqlite3_connection :database => ':memory:', + :adapter => 'sqlite3', + :timeout => 100 + assert_equal 'UTF-8', conn.encoding + end + end + end +end diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index e5fc1a2046..74588b4f47 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -121,34 +121,6 @@ class AggregationsTest < ActiveRecord::TestCase end end -class DeprecatedAggregationsTest < ActiveRecord::TestCase - class Person < ActiveRecord::Base; end - - def test_conversion_block_is_deprecated - assert_deprecated 'conversion block has been deprecated' do - Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money } - end - end - - def test_conversion_block_used_when_converter_option_is_nil - assert_deprecated 'conversion block has been deprecated' do - Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money } - end - assert_raise(NoMethodError) { Person.new.balance = 5 } - end - - def test_converter_option_overrides_conversion_block - assert_deprecated 'conversion block has been deprecated' do - Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| Money.new(balance) }) { |balance| balance.to_money } - end - - person = Person.new - assert_nothing_raised { person.balance = 5 } - assert_equal 5, person.balance.amount - assert_kind_of Money, person.balance - end -end - class OverridingAggregationsTest < ActiveRecord::TestCase class Name; end class DifferentName; end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 050b730dda..9c5dcc2ad9 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -41,9 +41,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_grafts_stashed_associations_to_correct_parent assert_nothing_raised do - Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('primary_contacts_people_2.id').all + Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('people.id').all end - assert_equal people(:michael), Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('primary_contacts_people_2.id').first + assert_equal people(:michael), Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('people.id').first end def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 45c7498013..5e3ba778f3 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -21,6 +21,68 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.destroyed_client_ids.clear end + def test_create_by + person = Person.create! :first_name => 'tenderlove' + post = Post.find :first + + assert_equal [], person.readers + assert_nil person.readers.find_by_post_id post.id + + reader = person.readers.create_by_post_id post.id + + assert_equal 1, person.readers.count + assert_equal 1, person.readers.length + assert_equal post, person.readers.first.post + assert_equal person, person.readers.first.person + end + + def test_create_by_multi + person = Person.create! :first_name => 'tenderlove' + post = Post.find :first + + assert_equal [], person.readers + + reader = person.readers.create_by_post_id_and_skimmer post.id, false + + assert_equal 1, person.readers.count + assert_equal 1, person.readers.length + assert_equal post, person.readers.first.post + assert_equal person, person.readers.first.person + end + + def test_find_or_create_by + person = Person.create! :first_name => 'tenderlove' + post = Post.find :first + + assert_equal [], person.readers + assert_nil person.readers.find_by_post_id post.id + + reader = person.readers.find_or_create_by_post_id post.id + + assert_equal 1, person.readers.count + assert_equal 1, person.readers.length + assert_equal post, person.readers.first.post + assert_equal person, person.readers.first.person + end + + def test_find_or_create + person = Person.create! :first_name => 'tenderlove' + post = Post.find :first + + assert_equal [], person.readers + assert_nil person.readers.find(:first, :conditions => { + :post_id => post.id + }) + + reader = person.readers.find_or_create :post_id => post.id + + assert_equal 1, person.readers.count + assert_equal 1, person.readers.length + assert_equal post, person.readers.first.post + assert_equal person, person.readers.first.person + end + + def force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.each {|f| } end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 4e4f9c385c..3b89c12a3f 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -712,7 +712,6 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end assert_raise(RuntimeError) { assert !@pirate.save } - assert before.first.frozen? # the first child was indeed destroyed assert_equal before, @pirate.reload.send(association_name) end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 5c175de6d4..ba7db838ca 100755..100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -798,25 +798,6 @@ class BasicsTest < ActiveRecord::TestCase assert_raise(NoMethodError) { t.title2 } end - def test_class_name - assert_equal "Firm", ActiveRecord::Base.class_name("firms") - assert_equal "Category", ActiveRecord::Base.class_name("categories") - assert_equal "AccountHolder", ActiveRecord::Base.class_name("account_holder") - - ActiveRecord::Base.pluralize_table_names = false - assert_equal "Firms", ActiveRecord::Base.class_name( "firms" ) - ActiveRecord::Base.pluralize_table_names = true - - ActiveRecord::Base.table_name_prefix = "test_" - assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms" ) - ActiveRecord::Base.table_name_suffix = "_tests" - assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms_tests" ) - ActiveRecord::Base.table_name_prefix = "" - assert_equal "Firm", ActiveRecord::Base.class_name( "firms_tests" ) - ActiveRecord::Base.table_name_suffix = "" - assert_equal "Firm", ActiveRecord::Base.class_name( "firms" ) - end - def test_null_fields assert_nil Topic.find(1).parent_id assert_nil Topic.create("title" => "Hey you").parent_id @@ -1401,6 +1382,14 @@ class BasicsTest < ActiveRecord::TestCase assert_not_equal clone.id, dev.id end + def test_clone_does_not_clone_associations + author = authors(:david) + assert_not_equal [], author.posts + + author_clone = author.clone + assert_equal [], author_clone.posts + end + def test_clone_preserves_subtype clone = nil assert_nothing_raised { clone = Company.find(3).clone } @@ -2076,10 +2065,6 @@ class BasicsTest < ActiveRecord::TestCase assert !SubStiPost.descends_from_active_record? end - def test_base_subclasses_is_public_method - assert ActiveRecord::Base.public_methods.map(&:to_sym).include?(:subclasses) - end - def test_find_on_abstract_base_class_doesnt_use_type_condition old_class = LooseDescendant Object.send :remove_const, :LooseDescendant @@ -2239,7 +2224,7 @@ class BasicsTest < ActiveRecord::TestCase def test_inspect_instance topic = topics(:first) - assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil>), topic.inspect + assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil, group: nil>), topic.inspect end def test_inspect_new_instance diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 329dd7d761..2c9d23c80f 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -272,7 +272,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_count_with_column_and_options_parameter - assert_equal 2, Account.count(:firm_id, :conditions => "credit_limit = 50") + assert_equal 2, Account.count(:firm_id, :conditions => "credit_limit = 50 AND firm_id IS NOT NULL") end def test_count_with_no_parameters_isnt_deprecated diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 377de168b9..377de168b9 100755..100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index c1c8f01e46..8c09fc4d59 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -173,7 +173,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_complex_inheritance very_special_client = VerySpecialClient.create("name" => "veryspecial") - assert_equal very_special_client, VerySpecialClient.find(:first, :conditions => "name = 'veryspecial'") + assert_equal very_special_client, VerySpecialClient.where("name = 'veryspecial'").first assert_equal very_special_client, SpecialClient.find(:first, :conditions => "name = 'veryspecial'") assert_equal very_special_client, Company.find(:first, :conditions => "name = 'veryspecial'") assert_equal very_special_client, Client.find(:first, :conditions => "name = 'veryspecial'") diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 1f544b4211..4aeae1fe45 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -1,20 +1,19 @@ require "cases/helper" require "models/developer" -require "rails/log_subscriber/test_helper" -require "active_record/railties/log_subscriber" +require "active_support/log_subscriber/test_helper" class LogSubscriberTest < ActiveSupport::TestCase - include Rails::LogSubscriber::TestHelper + include ActiveSupport::LogSubscriber::TestHelper def setup @old_logger = ActiveRecord::Base.logger super - - Rails::LogSubscriber.add(:active_record, ActiveRecord::Railties::LogSubscriber.new) + ActiveRecord::LogSubscriber.attach_to(:active_record) end def teardown super + ActiveRecord::LogSubscriber.log_subscribers.pop ActiveRecord::Base.logger = @old_logger end diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index e93f22bede..4e8ce1dac1 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -1,3 +1,7 @@ +# This file can be removed once with_exclusive_scope and with_scope are removed. +# All the tests were already ported to relation_scoping_test.rb when the new +# relation scoping API was added. + require "cases/helper" require 'models/post' require 'models/author' @@ -77,11 +81,13 @@ class MethodScopingTest < ActiveRecord::TestCase end end - def test_options_select_replaces_scope_select - Developer.send(:with_scope, :find => { :select => "id, name" }) do + def test_scope_select_concatenates + Developer.send(:with_scope, :find => { :select => "name" }) do developer = Developer.find(:first, :select => 'id, salary', :conditions => "name = 'David'") assert_equal 80000, developer.salary - assert !developer.has_attribute?(:name) + assert developer.has_attribute?(:id) + assert developer.has_attribute?(:name) + assert developer.has_attribute?(:salary) end end @@ -281,6 +287,12 @@ class NestedScopingTest < ActiveRecord::TestCase end end + def test_with_exclusive_scope_with_relation + assert_raise(ArgumentError) do + Developer.all_johns + end + end + def test_append_conditions Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do @@ -531,205 +543,4 @@ class NestedScopingTest < ActiveRecord::TestCase assert_equal 1, scoped_authors.size assert_equal authors(:david).attributes, scoped_authors.first.attributes end -end - -class HasManyScopingTest< ActiveRecord::TestCase - fixtures :comments, :posts - - def setup - @welcome = Post.find(1) - end - - def test_forwarding_of_static_methods - assert_equal 'a comment...', Comment.what_are_you - assert_equal 'a comment...', @welcome.comments.what_are_you - end - - def test_forwarding_to_scoped - assert_equal 4, Comment.search_by_type('Comment').size - assert_equal 2, @welcome.comments.search_by_type('Comment').size - end - - def test_forwarding_to_dynamic_finders - assert_equal 4, Comment.find_all_by_type('Comment').size - assert_equal 2, @welcome.comments.find_all_by_type('Comment').size - end - - def test_nested_scope - Comment.send(:with_scope, :find => { :conditions => '1=1' }) do - assert_equal 'a comment...', @welcome.comments.what_are_you - end - end -end - -class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase - fixtures :posts, :categories, :categories_posts - - def setup - @welcome = Post.find(1) - end - - def test_forwarding_of_static_methods - assert_equal 'a category...', Category.what_are_you - assert_equal 'a category...', @welcome.categories.what_are_you - end - - def test_forwarding_to_dynamic_finders - assert_equal 4, Category.find_all_by_type('SpecialCategory').size - assert_equal 0, @welcome.categories.find_all_by_type('SpecialCategory').size - assert_equal 2, @welcome.categories.find_all_by_type('Category').size - end - - def test_nested_scope - Category.send(:with_scope, :find => { :conditions => '1=1' }) do - assert_equal 'a comment...', @welcome.comments.what_are_you - end - end -end - -class DefaultScopingTest < ActiveRecord::TestCase - fixtures :developers, :posts - - def test_default_scope - expected = Developer.find(:all, :order => 'salary DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.find(:all).map(&:id).sort - assert_equal nil, DeveloperCalledDavid.create!.name - end - - def test_default_scope_with_conditions_hash - assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.find(:all).map(&:id).sort - assert_equal 'Jamis', DeveloperCalledJamis.create!.name - end - - def test_default_scoping_with_threads - 2.times do - Thread.new { assert_equal ['salary DESC'], DeveloperOrderedBySalary.scoped.order_values }.join - end - end - - def test_default_scoping_with_inheritance - # Inherit a class having a default scope and define a new default scope - klass = Class.new(DeveloperOrderedBySalary) - klass.send :default_scope, :limit => 1 - - # Scopes added on children should append to parent scope - assert_equal 1, klass.scoped.limit_value - assert_equal ['salary DESC'], klass.scoped.order_values - - # Parent should still have the original scope - assert_nil DeveloperOrderedBySalary.scoped.limit_value - assert_equal ['salary DESC'], DeveloperOrderedBySalary.scoped.order_values - end - - def test_default_scope_called_twice_merges_conditions - Developer.destroy_all - Developer.create!(:name => "David", :salary => 80000) - Developer.create!(:name => "David", :salary => 100000) - Developer.create!(:name => "Brian", :salary => 100000) - - klass = Class.new(Developer) - klass.__send__ :default_scope, :conditions => { :name => "David" } - klass.__send__ :default_scope, :conditions => { :salary => 100000 } - assert_equal 1, klass.count - assert_equal "David", klass.first.name - assert_equal 100000, klass.first.salary - end - def test_method_scope - expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_nested_scope - expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do - DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - end - assert_equal expected, received - end - - def test_named_scope_overwrites_default - expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } - assert_equal expected, received - end - - def test_nested_exclusive_scope - expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do - DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - end - assert_equal expected, received - end - - def test_overwriting_default_scope - expected = Developer.find(:all, :order => 'salary').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_default_scope_using_relation - posts = PostWithComment.scoped - assert_equal 2, posts.count - assert_equal posts(:thinking), posts.first - end - - def test_create_attribute_overwrites_default_scoping - assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name - assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary - end - - def test_create_attribute_overwrites_default_values - assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary - assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary - end -end - -=begin -# We disabled the scoping for has_one and belongs_to as we can't think of a proper use case - -class BelongsToScopingTest< ActiveRecord::TestCase - fixtures :comments, :posts - - def setup - @greetings = Comment.find(1) - end - - def test_forwarding_of_static_method - assert_equal 'a post...', Post.what_are_you - assert_equal 'a post...', @greetings.post.what_are_you - end - - def test_forwarding_to_dynamic_finders - assert_equal 4, Post.find_all_by_type('Post').size - assert_equal 1, @greetings.post.find_all_by_type('Post').size - end - -end - -class HasOneScopingTest< ActiveRecord::TestCase - fixtures :comments, :posts - - def setup - @sti_comments = Post.find(4) - end - - def test_forwarding_of_static_methods - assert_equal 'a comment...', Comment.what_are_you - assert_equal 'a very special comment...', @sti_comments.very_special_comment.what_are_you - end - - def test_forwarding_to_dynamic_finders - assert_equal 1, Comment.find_all_by_type('VerySpecialComment').size - assert_equal 1, @sti_comments.very_special_comment.find_all_by_type('VerySpecialComment').size - assert_equal 0, @sti_comments.very_special_comment.find_all_by_type('Comment').size - end - -end - -=end +end
\ No newline at end of file diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 6fe3b01281..4ce9bdb46d 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -128,9 +128,9 @@ if ActiveRecord::Base.connection.supports_migrations? good_index_name = 'x' * Person.connection.index_name_length too_long_index_name = good_index_name + 'x' assert_nothing_raised { Person.connection.add_index("people", "first_name", :name => too_long_index_name) } - assert !Person.connection.index_exists?("people", too_long_index_name, false) + assert !Person.connection.index_name_exists?("people", too_long_index_name, false) assert_nothing_raised { Person.connection.add_index("people", "first_name", :name => good_index_name) } - assert Person.connection.index_exists?("people", good_index_name, false) + assert Person.connection.index_name_exists?("people", good_index_name, false) end def test_remove_nonexistent_index @@ -146,8 +146,8 @@ if ActiveRecord::Base.connection.supports_migrations? Person.connection.add_index('people', [:first_name], :name => 'old_idx') assert_nothing_raised { Person.connection.rename_index('people', 'old_idx', 'new_idx') } # if the adapter doesn't support the indexes call, pick defaults that let the test pass - assert !Person.connection.index_exists?('people', 'old_idx', false) - assert Person.connection.index_exists?('people', 'new_idx', true) + assert !Person.connection.index_name_exists?('people', 'old_idx', false) + assert Person.connection.index_name_exists?('people', 'new_idx', true) end end @@ -158,6 +158,53 @@ if ActiveRecord::Base.connection.supports_migrations? end end + def test_index_exists + Person.connection.create_table :testings do |t| + t.column :foo, :string, :limit => 100 + t.column :bar, :string, :limit => 100 + end + Person.connection.add_index :testings, :foo + + assert Person.connection.index_exists?(:testings, :foo) + assert !Person.connection.index_exists?(:testings, :bar) + ensure + Person.connection.drop_table :testings rescue nil + end + + def test_index_exists_on_multiple_columns + Person.connection.create_table :testings do |t| + t.column :foo, :string, :limit => 100 + t.column :bar, :string, :limit => 100 + end + Person.connection.add_index :testings, [:foo, :bar] + + assert Person.connection.index_exists?(:testings, [:foo, :bar]) + ensure + Person.connection.drop_table :testings rescue nil + end + + def test_unique_index_exists + Person.connection.create_table :testings do |t| + t.column :foo, :string, :limit => 100 + end + Person.connection.add_index :testings, :foo, :unique => true + + assert Person.connection.index_exists?(:testings, :foo, :unique => true) + ensure + Person.connection.drop_table :testings rescue nil + end + + def test_named_index_exists + Person.connection.create_table :testings do |t| + t.column :foo, :string, :limit => 100 + end + Person.connection.add_index :testings, :foo, :name => "custom_index_name" + + assert Person.connection.index_exists?(:testings, :foo, :name => "custom_index_name") + ensure + Person.connection.drop_table :testings rescue nil + end + def testing_table_with_only_foo_attribute Person.connection.create_table :testings, :id => false do |t| t.column :foo, :string @@ -748,6 +795,10 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Base.connection.drop_table(:hats) end + def test_remove_column_no_second_parameter_raises_exception + assert_raise(ArgumentError) { Person.connection.remove_column("funny") } + end + def test_change_type_of_not_null_column assert_nothing_raised do Topic.connection.change_column "topics", "written_on", :datetime, :null => false @@ -970,6 +1021,45 @@ if ActiveRecord::Base.connection.supports_migrations? assert_nil Person.new.first_name end + def test_column_exists + Person.connection.create_table :testings do |t| + t.column :foo, :string + end + + assert Person.connection.column_exists?(:testings, :foo) + assert !Person.connection.column_exists?(:testings, :bar) + ensure + Person.connection.drop_table :testings rescue nil + end + + def test_column_exists_with_type + Person.connection.create_table :testings do |t| + t.column :foo, :string + t.column :bar, :decimal, :precision => 8, :scale => 2 + end + + assert Person.connection.column_exists?(:testings, :foo, :string) + assert !Person.connection.column_exists?(:testings, :foo, :integer) + assert Person.connection.column_exists?(:testings, :bar, :decimal) + assert !Person.connection.column_exists?(:testings, :bar, :integer) + ensure + Person.connection.drop_table :testings rescue nil + end + + def test_column_exists_with_definition + Person.connection.create_table :testings do |t| + t.column :foo, :string, :limit => 100 + t.column :bar, :decimal, :precision => 8, :scale => 2 + end + + assert Person.connection.column_exists?(:testings, :foo, :string, :limit => 100) + assert !Person.connection.column_exists?(:testings, :foo, :string, :limit => 50) + assert Person.connection.column_exists?(:testings, :bar, :decimal, :precision => 8, :scale => 2) + assert !Person.connection.column_exists?(:testings, :bar, :decimal, :precision => 10, :scale => 2) + ensure + Person.connection.drop_table :testings rescue nil + end + def test_add_table assert !Reminder.table_exists? @@ -1531,6 +1621,23 @@ if ActiveRecord::Base.connection.supports_migrations? end end + class ReservedWordsMigrationTest < ActiveRecord::TestCase + def test_drop_index_from_table_named_values + connection = Person.connection + connection.create_table :values, :force => true do |t| + t.integer :value + end + + assert_nothing_raised do + connection.add_index :values, :value + connection.remove_index :values, :column => :value + end + + connection.drop_table :values rescue nil + end + end + + class ChangeTableMigrationsTest < ActiveRecord::TestCase def setup @connection = Person.connection @@ -1680,6 +1787,20 @@ if ActiveRecord::Base.connection.supports_migrations? end end + def test_index_exists + with_change_table do |t| + @connection.expects(:index_exists?).with(:delete_me, :bar, {}) + t.index_exists?(:bar) + end + end + + def test_index_exists_with_options + with_change_table do |t| + @connection.expects(:index_exists?).with(:delete_me, :bar, {:unique => true}) + t.index_exists?(:bar, :unique => true) + end + end + def test_change_changes_column with_change_table do |t| @connection.expects(:change_column).with(:delete_me, :bar, :string, {}) diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 33ffb041c1..dc85b395d3 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -54,8 +54,8 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_respond_to_respects_include_private_parameter - assert !Topic.approved.respond_to?(:with_create_scope) - assert Topic.approved.respond_to?(:with_create_scope, true) + assert !Topic.approved.respond_to?(:tables_in_string) + assert Topic.approved.respond_to?(:tables_in_string, true) end def test_subclasses_inherit_scopes @@ -450,6 +450,12 @@ class NamedScopeTest < ActiveRecord::TestCase assert before.object_id != post.comments.containing_the_letter_e.object_id, "AssociationCollection##{method} should reset the named scopes cache" end end + + def test_named_scoped_are_lazy_loaded_if_table_still_does_not_exist + assert_nothing_raised do + require "models/without_table" + end + end end class DynamicScopeMatchTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 685b11cb03..3c797076e0 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -176,12 +176,6 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name end - def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record - assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}" do - @pirate.ship_attributes = { :id => 1234567890 } - end - end - def test_should_take_a_hash_with_string_keys_and_update_the_associated_model @pirate.reload.ship_attributes = { 'id' => @ship.id, 'name' => 'Davy Jones Gold Dagger' } @@ -331,11 +325,14 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase assert_equal 'Arr', @ship.pirate.catchphrase end - def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record - assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}" do - @ship.pirate_attributes = { :id => 1234567890 } - end - end + def test_should_associate_with_record_if_parent_record_is_not_saved + @ship.destroy + @pirate = Pirate.create(:catchphrase => 'Arr') + @ship = Ship.new(:name => 'Nights Dirty Lightning', :pirate_attributes => { :id => @pirate.id, :catchphrase => @pirate.catchphrase}) + + assert_equal @ship.name, 'Nights Dirty Lightning' + assert_equal @pirate, @ship.pirate + end def test_should_take_a_hash_with_string_keys_and_update_the_associated_model @ship.reload.pirate_attributes = { 'id' => @pirate.id, 'catchphrase' => 'Arr' } @@ -444,6 +441,11 @@ module NestedAttributesOnACollectionAssociationTests assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name] end + def test_should_assign_existing_children_if_parent_is_new + @pirate = Pirate.new({:catchphrase => "Don' botharr talkin' like one, savvy?"}.merge(@alternate_params)) + assert_equal ['Grace OMalley', 'Privateers Greed'], [@pirate.send(@association_name)[0].name, @pirate.send(@association_name)[1].name] + end + def test_should_also_work_with_a_HashWithIndifferentAccess @pirate.send(association_setter, HashWithIndifferentAccess.new('foo' => HashWithIndifferentAccess.new(:id => @child_1.id, :name => 'Grace OMalley'))) @pirate.save @@ -466,6 +468,27 @@ module NestedAttributesOnACollectionAssociationTests assert_equal 'Grace OMalley', @child_1.reload.name end + def test_should_not_overwrite_unsaved_updates_when_loading_association + @pirate.reload + @pirate.send(association_setter, [{ :id => @child_1.id, :name => 'Grace OMalley' }]) + assert_equal 'Grace OMalley', @pirate.send(@association_name).send(:load_target).find { |r| r.id == @child_1.id }.name + end + + def test_should_preserve_order_when_not_overwriting_unsaved_updates + @pirate.reload + @pirate.send(association_setter, [{ :id => @child_1.id, :name => 'Grace OMalley' }]) + assert_equal @child_1.id, @pirate.send(@association_name).send(:load_target).first.id + end + + def test_should_refresh_saved_records_when_not_overwriting_unsaved_updates + @pirate.reload + record = @pirate.class.reflect_on_association(@association_name).klass.new(:name => 'Grace OMalley') + @pirate.send(@association_name) << record + record.save! + @pirate.send(@association_name).last.update_attributes!(:name => 'Polly') + assert_equal 'Polly', @pirate.send(@association_name).send(:load_target).last.name + end + def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models @child_1.stubs(:id).returns('ABC1X') @child_2.stubs(:id).returns('ABC2X') @@ -480,8 +503,8 @@ module NestedAttributesOnACollectionAssociationTests assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name] end - def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record - assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}" do + def test_should_not_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record + assert_nothing_raised ActiveRecord::RecordNotFound do @pirate.attributes = { association_getter => [{ :id => 1234567890 }] } end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 67818622d7..03c4fc4e80 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -24,25 +24,25 @@ class ReflectionTest < ActiveRecord::TestCase def test_read_attribute_names assert_equal( - %w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id parent_title type ).sort, + %w( id title author_name author_email_address bonus_time written_on last_read content group approved replies_count parent_id parent_title type ).sort, @first.attribute_names ) end def test_columns - assert_equal 13, Topic.columns.length + assert_equal 14, Topic.columns.length end def test_columns_are_returned_in_the_order_they_were_declared column_names = Topic.columns.map { |column| column.name } - assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id parent_title type), column_names + assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id parent_title type group), column_names end def test_content_columns content_columns = Topic.content_columns content_column_names = content_columns.map {|column| column.name} - assert_equal 9, content_columns.length - assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved parent_title).sort, content_column_names.sort + assert_equal 10, content_columns.length + assert_equal %w(title author_name author_email_address written_on bonus_time last_read content group approved parent_title).sort, content_column_names.sort end def test_column_string_type_and_limit diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb new file mode 100644 index 0000000000..41dcdbcd37 --- /dev/null +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -0,0 +1,396 @@ +require "cases/helper" +require 'models/post' +require 'models/author' +require 'models/developer' +require 'models/project' +require 'models/comment' +require 'models/category' + +class RelationScopingTest < ActiveRecord::TestCase + fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects + + def test_scoped_find + Developer.where("name = 'David'").scoping do + assert_nothing_raised { Developer.find(1) } + end + end + + def test_scoped_find_first + developer = Developer.find(10) + Developer.where("salary = 100000").scoping do + assert_equal developer, Developer.order("name").first + end + end + + def test_scoped_find_last + highest_salary = Developer.order("salary DESC").first + + Developer.order("salary").scoping do + assert_equal highest_salary, Developer.last + end + end + + def test_scoped_find_last_preserves_scope + lowest_salary = Developer.first :order => "salary ASC" + highest_salary = Developer.first :order => "salary DESC" + + Developer.order("salary").scoping do + assert_equal highest_salary, Developer.last + assert_equal lowest_salary, Developer.first + end + end + + def test_scoped_find_combines_and_sanitizes_conditions + Developer.where("salary = 9000").scoping do + assert_equal developers(:poor_jamis), Developer.where("name = 'Jamis'").first + end + end + + def test_scoped_find_all + Developer.where("name = 'David'").scoping do + assert_equal [developers(:david)], Developer.all + end + end + + def test_scoped_find_select + Developer.select("id, name").scoping do + developer = Developer.where("name = 'David'").first + assert_equal "David", developer.name + assert !developer.has_attribute?(:salary) + end + end + + def test_scope_select_concatenates + Developer.select("id, name").scoping do + developer = Developer.select('id, salary').where("name = 'David'").first + assert_equal 80000, developer.salary + assert developer.has_attribute?(:id) + assert developer.has_attribute?(:name) + assert developer.has_attribute?(:salary) + end + end + + def test_scoped_count + Developer.where("name = 'David'").scoping do + assert_equal 1, Developer.count + end + + Developer.where('salary = 100000').scoping do + assert_equal 8, Developer.count + assert_equal 1, Developer.where("name LIKE 'fixture_1%'").count + end + end + + def test_scoped_find_include + # with the include, will retrieve only developers for the given project + scoped_developers = Developer.includes(:projects).scoping do + Developer.where('projects.id = 2').all + end + assert scoped_developers.include?(developers(:david)) + assert !scoped_developers.include?(developers(:jamis)) + assert_equal 1, scoped_developers.size + end + + def test_scoped_find_joins + scoped_developers = Developer.joins('JOIN developers_projects ON id = developer_id').scoping do + Developer.where('developers_projects.project_id = 2').all + end + + assert scoped_developers.include?(developers(:david)) + assert !scoped_developers.include?(developers(:jamis)) + assert_equal 1, scoped_developers.size + assert_equal developers(:david).attributes, scoped_developers.first.attributes + end + + def test_scoped_create_with_where + new_comment = VerySpecialComment.where(:post_id => 1).scoping do + VerySpecialComment.create :body => "Wonderful world" + end + + assert_equal 1, new_comment.post_id + assert Post.find(1).comments.include?(new_comment) + end + + def test_scoped_create_with_create_with + new_comment = VerySpecialComment.create_with(:post_id => 1).scoping do + VerySpecialComment.create :body => "Wonderful world" + end + + assert_equal 1, new_comment.post_id + assert Post.find(1).comments.include?(new_comment) + end + + def test_scoped_create_with_create_with_has_higher_priority + new_comment = VerySpecialComment.where(:post_id => 2).create_with(:post_id => 1).scoping do + VerySpecialComment.create :body => "Wonderful world" + end + + assert_equal 1, new_comment.post_id + assert Post.find(1).comments.include?(new_comment) + end + + def test_ensure_that_method_scoping_is_correctly_restored + scoped_methods = Developer.send(:current_scoped_methods) + + begin + Developer.where("name = 'Jamis'").scoping do + raise "an exception" + end + rescue + end + + assert_equal scoped_methods, Developer.send(:current_scoped_methods) + end +end + +class NestedRelationScopingTest < ActiveRecord::TestCase + fixtures :authors, :developers, :projects, :comments, :posts + + def test_merge_options + Developer.where('salary = 80000').scoping do + Developer.limit(10).scoping do + devs = Developer.scoped + assert_equal '(salary = 80000)', devs.arel.send(:where_clauses).join(' AND ') + assert_equal 10, devs.taken + end + end + end + + def test_merge_inner_scope_has_priority + Developer.limit(5).scoping do + Developer.limit(10).scoping do + assert_equal 10, Developer.count + end + end + end + + def test_replace_options + Developer.where(:name => 'David').scoping do + Developer.unscoped do + assert_equal 'Jamis', Developer.where(:name => 'Jamis').first[:name] + end + + assert_equal 'David', Developer.first[:name] + end + end + + def test_three_level_nested_exclusive_scoped_find + Developer.where("name = 'Jamis'").scoping do + assert_equal 'Jamis', Developer.first.name + + Developer.unscoped.where("name = 'David'") do + assert_equal 'David', Developer.first.name + + Developer.unscoped.where("name = 'Maiha'") do + assert_equal nil, Developer.first + end + + # ensure that scoping is restored + assert_equal 'David', Developer.first.name + end + + # ensure that scoping is restored + assert_equal 'Jamis', Developer.first.name + end + end + + def test_nested_scoped_create + comment = Comment.create_with(:post_id => 1).scoping do + Comment.create_with(:post_id => 2).scoping do + Comment.create :body => "Hey guys, nested scopes are broken. Please fix!" + end + end + + assert_equal 2, comment.post_id + end + + def test_nested_exclusive_scope_for_create + comment = Comment.create_with(:body => "Hey guys, nested scopes are broken. Please fix!").scoping do + Comment.unscoped.create_with(:post_id => 1).scoping do + assert Comment.new.body.blank? + Comment.create :body => "Hey guys" + end + end + + assert_equal 1, comment.post_id + assert_equal 'Hey guys', comment.body + end +end + +class HasManyScopingTest< ActiveRecord::TestCase + fixtures :comments, :posts + + def setup + @welcome = Post.find(1) + end + + def test_forwarding_of_static_methods + assert_equal 'a comment...', Comment.what_are_you + assert_equal 'a comment...', @welcome.comments.what_are_you + end + + def test_forwarding_to_scoped + assert_equal 4, Comment.search_by_type('Comment').size + assert_equal 2, @welcome.comments.search_by_type('Comment').size + end + + def test_forwarding_to_dynamic_finders + assert_equal 4, Comment.find_all_by_type('Comment').size + assert_equal 2, @welcome.comments.find_all_by_type('Comment').size + end + + def test_nested_scope_finder + Comment.where('1=0').scoping do + assert_equal 0, @welcome.comments.count + assert_equal 'a comment...', @welcome.comments.what_are_you + end + + Comment.where('1=1').scoping do + assert_equal 2, @welcome.comments.count + assert_equal 'a comment...', @welcome.comments.what_are_you + end + end +end + +class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase + fixtures :posts, :categories, :categories_posts + + def setup + @welcome = Post.find(1) + end + + def test_forwarding_of_static_methods + assert_equal 'a category...', Category.what_are_you + assert_equal 'a category...', @welcome.categories.what_are_you + end + + def test_forwarding_to_dynamic_finders + assert_equal 4, Category.find_all_by_type('SpecialCategory').size + assert_equal 0, @welcome.categories.find_all_by_type('SpecialCategory').size + assert_equal 2, @welcome.categories.find_all_by_type('Category').size + end + + def test_nested_scope_finder + Category.where('1=0').scoping do + assert_equal 0, @welcome.categories.count + assert_equal 'a category...', @welcome.categories.what_are_you + end + + Category.where('1=1').scoping do + assert_equal 2, @welcome.categories.count + assert_equal 'a category...', @welcome.categories.what_are_you + end + end +end + +class DefaultScopingTest < ActiveRecord::TestCase + fixtures :developers, :posts + + def test_default_scope + expected = Developer.find(:all, :order => 'salary DESC').collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + assert_equal expected, received + end + + def test_default_scope_is_unscoped_on_find + assert_equal 1, DeveloperCalledDavid.count + assert_equal 11, DeveloperCalledDavid.unscoped.count + end + + def test_default_scope_is_unscoped_on_create + assert_nil DeveloperCalledJamis.unscoped.create!.name + end + + def test_default_scope_with_conditions_string + assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.find(:all).map(&:id).sort + assert_equal nil, DeveloperCalledDavid.create!.name + end + + def test_default_scope_with_conditions_hash + assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.find(:all).map(&:id).sort + assert_equal 'Jamis', DeveloperCalledJamis.create!.name + end + + def test_default_scoping_with_threads + 2.times do + Thread.new { assert_equal ['salary DESC'], DeveloperOrderedBySalary.scoped.order_values }.join + end + end + + def test_default_scoping_with_inheritance + # Inherit a class having a default scope and define a new default scope + klass = Class.new(DeveloperOrderedBySalary) + klass.send :default_scope, :limit => 1 + + # Scopes added on children should append to parent scope + assert_equal 1, klass.scoped.limit_value + assert_equal ['salary DESC'], klass.scoped.order_values + + # Parent should still have the original scope + assert_nil DeveloperOrderedBySalary.scoped.limit_value + assert_equal ['salary DESC'], DeveloperOrderedBySalary.scoped.order_values + end + + def test_default_scope_called_twice_merges_conditions + Developer.destroy_all + Developer.create!(:name => "David", :salary => 80000) + Developer.create!(:name => "David", :salary => 100000) + Developer.create!(:name => "Brian", :salary => 100000) + + klass = Class.new(Developer) + klass.__send__ :default_scope, :conditions => { :name => "David" } + klass.__send__ :default_scope, :conditions => { :salary => 100000 } + assert_equal 1, klass.count + assert_equal "David", klass.first.name + assert_equal 100000, klass.first.salary + end + def test_method_scope + expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } + assert_equal expected, received + end + + def test_nested_scope + expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do + DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + end + assert_equal expected, received + end + + def test_named_scope_overwrites_default + expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_nested_exclusive_scope + expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do + DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + end + assert_equal expected, received + end + + def test_overwriting_default_scope + expected = Developer.find(:all, :order => 'salary').collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } + assert_equal expected, received + end + + def test_default_scope_using_relation + posts = PostWithComment.scoped + assert_equal 2, posts.count + assert_equal posts(:thinking), posts.first + end + + def test_create_attribute_overwrites_default_scoping + assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name + assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary + end + + def test_create_attribute_overwrites_default_values + assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary + assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary + end +end
\ No newline at end of file diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 43519db976..ffde8daa07 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -16,12 +16,34 @@ class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, :taggings + def test_apply_relation_as_where_id + posts = Post.arel_table + post_authors = posts.where(posts[:author_id].eq(1)).project(posts[:id]) + assert_equal 5, post_authors.to_a.size + assert_equal 5, Post.where(:id => post_authors).size + end + def test_scoped topics = Topic.scoped assert_kind_of ActiveRecord::Relation, topics assert_equal 4, topics.size end + def test_to_json + assert_nothing_raised { Bird.scoped.to_json } + assert_nothing_raised { Bird.scoped.all.to_json } + end + + def test_to_yaml + assert_nothing_raised { Bird.scoped.to_yaml } + assert_nothing_raised { Bird.scoped.all.to_yaml } + end + + def test_to_xml + assert_nothing_raised { Bird.scoped.to_xml } + assert_nothing_raised { Bird.scoped.all.to_xml } + end + def test_scoped_all topics = Topic.scoped.all assert_kind_of Array, topics @@ -88,6 +110,18 @@ class RelationTest < ActiveRecord::TestCase assert_equal topics(:first).title, topics.first.title end + def test_finding_with_order_concatenated + topics = Topic.order('author_name').order('title') + assert_equal 4, topics.to_a.size + assert_equal topics(:fourth).title, topics.first.title + end + + def test_finding_with_reorder + topics = Topic.order('author_name').order('title').reorder('id') + assert_equal 4, topics.to_a.size + assert_equal topics(:first).title, topics.first.title + end + def test_finding_with_order_and_take entrants = Entrant.order("id ASC").limit(2).to_a @@ -461,7 +495,7 @@ class RelationTest < ActiveRecord::TestCase posts = Post.scoped assert_equal [0], posts.select('comments_count').where('id is not null').group('id').order('id').count.values.uniq - assert_equal 0, posts.where('id is not null').select('comments_count').count + assert_equal 7, posts.where('id is not null').select('comments_count').count assert_equal 7, posts.select('comments_count').count('id') assert_equal 0, posts.select('comments_count').count @@ -469,6 +503,12 @@ class RelationTest < ActiveRecord::TestCase assert_equal 0, posts.count('comments_count') end + def test_multiple_selects + post = Post.scoped.select('comments_count').select('title').order("id ASC").first + assert_equal "Welcome to the weblog", post.title + assert_equal 2, post.comments_count + end + def test_size posts = Post.scoped @@ -503,13 +543,13 @@ class RelationTest < ActiveRecord::TestCase def test_many posts = Post.scoped - + assert_queries(2) do assert posts.many? # Uses COUNT() assert posts.many? {|p| p.id > 0 } assert ! posts.many? {|p| p.id < 2 } end - + assert posts.loaded? end @@ -578,7 +618,7 @@ class RelationTest < ActiveRecord::TestCase end def test_anonymous_extension - relation = Post.where(:author_id => 1).order('id ASC') do + relation = Post.where(:author_id => 1).order('id ASC').extending do def author 'lifo' end @@ -593,4 +633,12 @@ class RelationTest < ActiveRecord::TestCase assert_equal "lifo", relation.author assert_equal "lifo", relation.limit(1).author end + + def test_order_by_relation_attribute + assert_equal Post.order(Post.arel_table[:title]).all, Post.order("title").all + end + + def test_relations_limit_with_conditions_or_limit + assert_equal Post.limit(2).size, Post.limit(2).all.size + end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index ebc16653cb..df123c9de8 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -221,20 +221,28 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal 2, @first.rollbacks end - def test_after_transaction_callbacks_should_not_raise_errors + def test_after_transaction_callbacks_should_prevent_callbacks_from_being_called def @first.last_after_transaction_error=(e); @last_transaction_error = e; end def @first.last_after_transaction_error; @last_transaction_error; end @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";} @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";} + @second.after_commit_block{|r| r.history << :after_commit} + @second.after_rollback_block{|r| r.history << :after_rollback} - @first.save! + Topic.transaction do + @first.save! + @second.save! + end assert_equal :commit, @first.last_after_transaction_error + assert_equal [:after_commit], @second.history + @second.history.clear Topic.transaction do @first.save! + @second.save! raise ActiveRecord::Rollback end - assert_equal :rollback, @first.last_after_transaction_error + assert_equal [:after_rollback], @second.history end end diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index 8ee2a5868c..454e42ed37 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -9,22 +9,22 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase I18n.backend = I18n::Backend::Simple.new end - # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + # validates_associated: generate_message(attr_name, :invalid, :message => custom_message, :value => value) def test_generate_message_invalid_with_default_message - assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title') + assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :value => 'title') end def test_generate_message_invalid_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message %{value}', :value => 'title') + assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :message => 'custom message %{value}', :value => 'title') end - # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message]) + # validates_uniqueness_of: generate_message(attr_name, :taken, :message => custom_message) def test_generate_message_taken_with_default_message - assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title') + assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :value => 'title') end def test_generate_message_taken_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message %{value}', :value => 'title') + assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :message => 'custom message %{value}', :value => 'title') end # ActiveRecord#RecordInvalid exception diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 38fa2b821d..15b97c02c8 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -31,34 +31,40 @@ class I18nValidationTest < ActiveRecord::TestCase end end - # validates_uniqueness_of w/ mocha + # A set of common cases for ActiveModel::Validations message generation that + # are used to generate tests to keep things DRY + # + COMMON_CASES = [ + # [ case, validation_options, generate_message_options] + [ "given no options", {}, {}], + [ "given custom message", {:message => "custom"}, {:message => "custom"}], + [ "given if condition", {:if => lambda { true }}, {}], + [ "given unless condition", {:unless => lambda { false }}, {}], + [ "given option that is not reserved", {:format => "jpg"}, {:format => "jpg" }] + # TODO Add :on case, but below doesn't work, because then the validation isn't run for some reason + # even when using .save instead .valid? + # [ "given on condition", {:on => :save}, {}] + ] - def test_validates_uniqueness_of_generates_message - Topic.validates_uniqueness_of :title - @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil, :value => 'unique!'}) - @topic.valid? - end + # validates_uniqueness_of w/ mocha - def test_validates_uniqueness_of_generates_message_with_custom_default_message - Topic.validates_uniqueness_of :title, :message => 'custom' - @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom', :value => 'unique!'}) - @topic.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_uniqueness_of on generated message #{name}" do + Topic.validates_uniqueness_of :title, validation_options + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, generate_message_options.merge(:value => 'unique!')) + @topic.valid? + end end # validates_associated w/ mocha - def test_validates_associated_generates_message - Topic.validates_associated :replies - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) - replied_topic.valid? - end - - def test_validates_associated_generates_message_with_custom_default_message - Topic.validates_associated :replies - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) - replied_topic.valid? + COMMON_CASES.each do |name, validation_options, generate_message_options| + test "validates_associated on generated message #{name}" do + Topic.validates_associated :replies, validation_options + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies)) + replied_topic.save + end end # validates_associated w/o mocha diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml index 1769152445..93f48aedc4 100644 --- a/activerecord/test/fixtures/topics.yml +++ b/activerecord/test/fixtures/topics.yml @@ -24,7 +24,7 @@ second: third: id: 3 title: The Third Topic of the day - author_name: Nick + author_name: Carl written_on: 2005-07-15t15:28:00.0099+01:00 content: I'm a troll approved: true diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index e35de3b9b0..de68fd7f24 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -55,6 +55,12 @@ class Developer < ActiveRecord::Base def log=(message) audit_logs.build :message => message end + + def self.all_johns + self.with_exclusive_scope :find => where(:name => 'John') do + self.all + end + end end class AuditLog < ActiveRecord::Base @@ -103,4 +109,4 @@ end class PoorDeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' default_scope :conditions => { :name => 'Jamis', :salary => 50000 } -end
\ No newline at end of file +end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index f1dbe32c6e..d89c8cf381 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -1,7 +1,7 @@ class Pirate < ActiveRecord::Base belongs_to :parrot, :validate => true belongs_to :non_validated_parrot, :class_name => 'Parrot' - has_and_belongs_to_many :parrots, :validate => true + has_and_belongs_to_many :parrots, :validate => true, :order => 'parrots.id ASC' has_and_belongs_to_many :non_validated_parrots, :class_name => 'Parrot' has_and_belongs_to_many :parrots_with_method_callbacks, :class_name => "Parrot", :before_add => :log_before_add, @@ -21,7 +21,7 @@ class Pirate < ActiveRecord::Base has_one :ship has_one :update_only_ship, :class_name => 'Ship' has_one :non_validated_ship, :class_name => 'Ship' - has_many :birds + has_many :birds, :order => 'birds.id ASC' has_many :birds_with_method_callbacks, :class_name => "Bird", :before_add => :log_before_add, :after_add => :log_after_add, diff --git a/activerecord/test/models/without_table.rb b/activerecord/test/models/without_table.rb new file mode 100644 index 0000000000..87f80911e1 --- /dev/null +++ b/activerecord/test/models/without_table.rb @@ -0,0 +1,3 @@ +class WithoutTable < ActiveRecord::Base + default_scope where(:published => true) +end
\ No newline at end of file diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index f5fba2f87d..b212e7cff2 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -485,6 +485,7 @@ ActiveRecord::Schema.define do t.integer :parent_id t.string :parent_title t.string :type + t.string :group end create_table :taggings, :force => true do |t| diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index c1ec8559f5..6c494a8bcc 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -17,6 +17,7 @@ require 'active_resource/exceptions' require 'active_resource/connection' require 'active_resource/formats' require 'active_resource/schema' +require 'active_resource/log_subscriber' module ActiveResource # ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application. @@ -763,7 +764,7 @@ module ActiveResource # With any other scope, find returns nil when no data is returned. # # Person.find(1) - # # => raises ResourcenotFound + # # => raises ResourceNotFound # # Person.find(:all) # Person.find(:first) @@ -941,7 +942,7 @@ module ActiveResource end # This is a list of known attributes for this resource. Either - # gathered fromthe provided <tt>schema</tt>, or from the attributes + # gathered from the provided <tt>schema</tt>, or from the attributes # set on this instance after it has been fetched from the remote system. def known_attributes self.class.known_attributes + self.attributes.keys.map(&:to_s) diff --git a/activeresource/lib/active_resource/http_mock.rb b/activeresource/lib/active_resource/http_mock.rb index 1ed3804017..f192c53b4f 100644 --- a/activeresource/lib/active_resource/http_mock.rb +++ b/activeresource/lib/active_resource/http_mock.rb @@ -148,16 +148,28 @@ module ActiveResource attr_accessor :path, :method, :body, :headers def initialize(method, path, body = nil, headers = {}) - @method, @path, @body, @headers = method, path, body, headers.merge(ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[method] => 'application/xml') + @method, @path, @body, @headers = method, path, body, headers end def ==(req) - path == req.path && method == req.method && headers == req.headers + path == req.path && method == req.method && headers_match?(req) end def to_s "<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>" end + + private + + def headers_match?(req) + # Ignore format header on equality if it's not defined + format_header = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[method] + if headers[format_header].present? || req.headers[format_header].blank? + headers == req.headers + else + headers.dup.merge(format_header => req.headers[format_header]) == req.headers + end + end end class Response diff --git a/activeresource/lib/active_resource/log_subscriber.rb b/activeresource/lib/active_resource/log_subscriber.rb new file mode 100644 index 0000000000..9e52baf36d --- /dev/null +++ b/activeresource/lib/active_resource/log_subscriber.rb @@ -0,0 +1,15 @@ +module ActiveResource + class LogSubscriber < ActiveSupport::LogSubscriber + def request(event) + result = event.payload[:result] + info "#{event.payload[:method].to_s.upcase} #{event.payload[:request_uri]}" + info "--> %d %s %d (%.1fms)" % [result.code, result.message, result.body.to_s.length, event.duration] + end + + def logger + ActiveResource::Base.logger + end + end +end + +ActiveResource::LogSubscriber.attach_to :active_resource
\ No newline at end of file diff --git a/activeresource/lib/active_resource/railtie.rb b/activeresource/lib/active_resource/railtie.rb index aa878c7212..60f6f88311 100644 --- a/activeresource/lib/active_resource/railtie.rb +++ b/activeresource/lib/active_resource/railtie.rb @@ -5,9 +5,6 @@ module ActiveResource class Railtie < Rails::Railtie config.active_resource = ActiveSupport::OrderedOptions.new - require "active_resource/railties/log_subscriber" - log_subscriber :active_resource, ActiveResource::Railties::LogSubscriber.new - initializer "active_resource.set_configs" do |app| app.config.active_resource.each do |k,v| ActiveResource::Base.send "#{k}=", v diff --git a/activeresource/lib/active_resource/railties/log_subscriber.rb b/activeresource/lib/active_resource/railties/log_subscriber.rb deleted file mode 100644 index 86806a93d0..0000000000 --- a/activeresource/lib/active_resource/railties/log_subscriber.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveResource - module Railties - class LogSubscriber < Rails::LogSubscriber - def request(event) - result = event.payload[:result] - info "#{event.payload[:method].to_s.upcase} #{event.payload[:request_uri]}" - info "--> %d %s %d (%.1fms)" % [result.code, result.message, result.body.to_s.length, event.duration] - end - - def logger - ActiveResource::Base.logger - end - end - end -end
\ No newline at end of file diff --git a/activeresource/lib/active_resource/validations.rb b/activeresource/lib/active_resource/validations.rb index a19e0d0ac9..026d81e44a 100644 --- a/activeresource/lib/active_resource/validations.rb +++ b/activeresource/lib/active_resource/validations.rb @@ -27,7 +27,7 @@ module ActiveResource # Grabs errors from a json response. def from_json(json, save_cache = false) - array = ActiveSupport::JSON.decode(json)['errors'] rescue [] + array = Array.wrap(ActiveSupport::JSON.decode(json)['errors']) rescue [] from_array array, save_cache end diff --git a/activeresource/test/cases/base_errors_test.rb b/activeresource/test/cases/base_errors_test.rb index b4fd75fba3..5063916d10 100644 --- a/activeresource/test/cases/base_errors_test.rb +++ b/activeresource/test/cases/base_errors_test.rb @@ -17,7 +17,7 @@ class BaseErrorsTest < Test::Unit::TestCase end end - def test_should_parse_xml_errors + def test_should_parse_json_and_xml_errors [ :json, :xml ].each do |format| invalid_user_using_format(format) do assert_kind_of ActiveResource::Errors, @person.errors @@ -26,6 +26,17 @@ class BaseErrorsTest < Test::Unit::TestCase end end + def test_should_parse_json_errors_when_no_errors_key + ActiveResource::HttpMock.respond_to do |mock| + mock.post "/people.json", {}, '{}', 422, {'Content-Type' => 'application/json; charset=utf-8'} + end + + invalid_user_using_format(:json) do + assert_kind_of ActiveResource::Errors, @person.errors + assert_equal 0, @person.errors.size + end + end + def test_should_parse_errors_to_individual_attributes [ :json, :xml ].each do |format| invalid_user_using_format(format) do diff --git a/activeresource/test/cases/http_mock_test.rb b/activeresource/test/cases/http_mock_test.rb new file mode 100644 index 0000000000..5e032d03f1 --- /dev/null +++ b/activeresource/test/cases/http_mock_test.rb @@ -0,0 +1,71 @@ +require 'abstract_unit' + +class HttpMockTest < ActiveSupport::TestCase + setup do + @http = ActiveResource::HttpMock.new("http://example.com") + end + + FORMAT_HEADER = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES + + [:post, :put, :get, :delete, :head].each do |method| + test "responds to simple #{method} request" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/xml"}, "Response") + end + + assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body + end + + test "adds format header by default to #{method} request" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(method, "/people/1", {}, "Response") + end + + assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body + end + + test "respond only when headers match header by default to #{method} request" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(method, "/people/1", {"X-Header" => "X"}, "Response") + end + + assert_equal "Response", request(method, "/people/1", "X-Header" => "X").body + assert_raise(ActiveResource::InvalidRequestError) { request(method, "/people/1") } + end + + test "does not overwrite format header to #{method} request" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/json"}, "Response") + end + + assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/json").body + end + + test "ignores format header when there is only one response to same url in a #{method} request" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(method, "/people/1", {}, "Response") + end + + assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/json").body + assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body + end + + test "responds correctly when format header is given to #{method} request" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/xml"}, "XML") + mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/json"}, "Json") + end + + assert_equal "XML", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body + assert_equal "Json", request(method, "/people/1", FORMAT_HEADER[method] => "application/json").body + end + end + + def request(method, path, headers = {}, body = nil) + if [:put, :post].include? method + @http.send(method, path, body, headers) + else + @http.send(method, path, headers) + end + end +end diff --git a/activeresource/test/cases/log_subscriber_test.rb b/activeresource/test/cases/log_subscriber_test.rb index f0330e8f51..3cd96007db 100644 --- a/activeresource/test/cases/log_subscriber_test.rb +++ b/activeresource/test/cases/log_subscriber_test.rb @@ -1,12 +1,11 @@ require "abstract_unit" require "fixtures/person" -require "rails/log_subscriber/test_helper" -require "active_resource/railties/log_subscriber" +require "active_support/log_subscriber/test_helper" +require "active_resource/log_subscriber" require "active_support/core_ext/hash/conversions" -# TODO: This test should be part of Railties class LogSubscriberTest < ActiveSupport::TestCase - include Rails::LogSubscriber::TestHelper + include ActiveSupport::LogSubscriber::TestHelper def setup super @@ -16,7 +15,7 @@ class LogSubscriberTest < ActiveSupport::TestCase mock.get "/people/1.xml", {}, @matz end - Rails::LogSubscriber.add(:active_resource, ActiveResource::Railties::LogSubscriber.new) + ActiveResource::LogSubscriber.attach_to :active_resource end def set_logger(logger) diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 13ec3c3775..60586cf99a 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,17 @@ *Rails 3.0.0 [Release Candidate] (unreleased)* +* Deprecation behavior is no longer hardcoded to the name of the environment. + Instead, it is set via config.active_support.deprecation and can be one + of :log, :stderr or :notify. :notify is a new style that sends the warning + via ActiveSupport::Notifications, and is the new default for production + [Yehuda Katz] + +* Renamed ActiveSupport::Dependecies.load_(once_)paths to autoload_(once_)paths. [fxn] + +* Added ActiveSupport::FileUpdateChecker to execute a block only if a set of files changed, used by Router and I18n locale files. [José Valim] + +* Added ActiveSupport::DescendantsTracker to track descendants with support to constants reloading. [José Valim] + * ActiveSupport::OrderedHash#merge and #merge! accept a block. #4838 [Paul Mucur, fxn] * Date#since, #ago, #beginning_of_day, #end_of_day, and #xmlschema honor now the user time zone if set. [Geoff Buesing] diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index 51edb59c77..51edb59c77 100755..100644 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index e34e46b4cf..ba91e8bba3 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -39,6 +39,11 @@ require "active_support/dependencies/autoload" module ActiveSupport extend ActiveSupport::Autoload + autoload :DescendantsTracker + autoload :FileUpdateChecker + autoload :LogSubscriber + autoload :Notifications + # TODO: Narrow this list down eager_autoload do autoload :BacktraceCleaner @@ -61,7 +66,6 @@ module ActiveSupport autoload :OptionMerger autoload :OrderedHash autoload :OrderedOptions - autoload :Notifications autoload :Rescuable autoload :SecureRandom autoload :StringInquirer diff --git a/activesupport/lib/active_support/all.rb b/activesupport/lib/active_support/all.rb index f537818300..def8eca89f 100644 --- a/activesupport/lib/active_support/all.rb +++ b/activesupport/lib/active_support/all.rb @@ -1,3 +1,4 @@ require 'active_support' require 'active_support/time' require 'active_support/core_ext' +require 'active_support/json' diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 2605a3f2b8..bef9c98ecf 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -22,7 +22,7 @@ module ActiveSupport EMPTY_OPTIONS = {}.freeze # These options mean something to all cache implementations. Individual cache - # implementations may support additional optons. + # implementations may support additional options. UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl] module Strategy @@ -267,27 +267,39 @@ module ActiveSupport # :bar # end # cache.fetch("foo") # => "bar" - def fetch(name, options = nil, &block) - options = merged_options(options) - key = namespaced_key(name, options) - entry = instrument(:read, name, options) { read_entry(key, options) } unless options[:force] - if entry && entry.expired? - race_ttl = options[:race_condition_ttl].to_f - if race_ttl and Time.now.to_f - entry.expires_at <= race_ttl - entry.expires_at = Time.now + race_ttl - write_entry(key, entry, :expires_in => race_ttl * 2) - else - delete_entry(key, options) + def fetch(name, options = nil) + if block_given? + options = merged_options(options) + key = namespaced_key(name, options) + unless options[:force] + entry = instrument(:read, name, options) do |payload| + payload[:super_operation] = :fetch if payload + read_entry(key, options) + end + end + if entry && entry.expired? + race_ttl = options[:race_condition_ttl].to_f + if race_ttl and Time.now.to_f - entry.expires_at <= race_ttl + entry.expires_at = Time.now + race_ttl + write_entry(key, entry, :expires_in => race_ttl * 2) + else + delete_entry(key, options) + end + entry = nil end - entry = nil - end - if entry - entry.value - elsif block_given? - result = instrument(:generate, name, options, &block) - write(name, result, options) - result + if entry + instrument(:fetch_hit, name, options) { |payload| } + entry.value + else + result = instrument(:generate, name, options) do |payload| + yield + end + write(name, result, options) + result + end + else + read(name, options) end end @@ -299,16 +311,19 @@ module ActiveSupport def read(name, options = nil) options = merged_options(options) key = namespaced_key(name, options) - instrument(:read, name, options) do + instrument(:read, name, options) do |payload| entry = read_entry(key, options) if entry if entry.expired? delete_entry(key, options) + payload[:hit] = false if payload nil else + payload[:hit] = true if payload entry.value end else + payload[:hit] = false if payload nil end end @@ -345,7 +360,7 @@ module ActiveSupport # +options+. def write(name, value, options = nil) options = merged_options(options) - instrument(:write, name, options) do + instrument(:write, name, options) do |payload| entry = Entry.new(value, options) write_entry(namespaced_key(name, options), entry, options) end @@ -356,7 +371,7 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def delete(name, options = nil) options = merged_options(options) - instrument(:delete, name) do + instrument(:delete, name) do |payload| delete_entry(namespaced_key(name, options), options) end end @@ -366,7 +381,7 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def exist?(name, options = nil) options = merged_options(options) - instrument(:exist?, name) do + instrument(:exist?, name) do |payload| entry = read_entry(namespaced_key(name, options), options) if entry && !entry.expired? true @@ -502,9 +517,9 @@ module ActiveSupport if self.class.instrument payload = { :key => key } payload.merge!(options) if options.is_a?(Hash) - ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield } + ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } else - yield + yield(nil) end end diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index e3a2688e2f..852defeae8 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -35,7 +35,7 @@ module ActiveSupport def self.build_mem_cache(*addresses) addresses = addresses.flatten options = addresses.extract_options! - addresses = ["localhost"] if addresses.empty? + addresses = ["localhost:11211"] if addresses.empty? MemCache.new(addresses, options) end @@ -159,7 +159,7 @@ module ActiveSupport private def escape_key(key) - key = key.to_s.gsub(ESCAPE_KEY_CHARS){|match| "%#{match[0].to_s(16).upcase}"} + key = key.to_s.gsub(ESCAPE_KEY_CHARS){|match| "%#{match.getbyte(0).to_s(16).upcase}"} key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250 key end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index b1d14a0d8f..f5c2b8af8b 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -10,7 +10,7 @@ module ActiveSupport # appropriate cache for you. # # This cache has a bounded size specified by the :size options to the - # initializer (default is 32Mb). When the cache exceeds the alotted size, + # initializer (default is 32Mb). When the cache exceeds the allotted size, # a cleanup will occur which tries to prune the cache down to three quarters # of the maximum size by removing the least recently used entries. # diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 3ff33eea72..c4e1eb2c04 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -1,6 +1,6 @@ +require 'active_support/descendants_tracker' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/inheritable_attributes' -require 'active_support/core_ext/class/subclasses' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' @@ -85,6 +85,10 @@ module ActiveSupport module Callbacks extend Concern + included do + extend ActiveSupport::DescendantsTracker + end + def run_callbacks(kind, *args, &block) send("_run_#{kind}_callbacks", *args, &block) end @@ -428,7 +432,7 @@ module ActiveSupport options = filters.last.is_a?(Hash) ? filters.pop : {} filters.unshift(block) if block - ([self] + self.descendents).each do |target| + ([self] + self.descendants).each do |target| chain = target.send("_#{name}_callbacks") yield chain, type, filters, options target.__define_runner(name) @@ -502,7 +506,7 @@ module ActiveSupport def reset_callbacks(symbol) callbacks = send("_#{symbol}_callbacks") - self.descendents.each do |target| + self.descendants.each do |target| chain = target.send("_#{symbol}_callbacks") callbacks.each { |c| chain.delete(c) } target.__define_runner(symbol) diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index ef416787a9..4cd9bfadac 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -55,7 +55,7 @@ class Array # ["6", "7"] def in_groups(number, fill_with = nil) # size / number gives minor group size; - # size % number gives how many objects need extra accomodation; + # size % number gives how many objects need extra accommodation; # each group hold either division or division + 1 items. division = size / number modulo = size % number diff --git a/activesupport/lib/active_support/core_ext/array/random_access.rb b/activesupport/lib/active_support/core_ext/array/random_access.rb index 7a4836cecd..edac7278bc 100644 --- a/activesupport/lib/active_support/core_ext/array/random_access.rb +++ b/activesupport/lib/active_support/core_ext/array/random_access.rb @@ -1,5 +1,12 @@ class Array # Backport of Array#sample based on Marc-Andre Lafortune's http://github.com/marcandre/backports/ + # Returns a random element or +n+ random elements from the array. + # If the array is empty and +n+ is nil, returns <tt>nil</tt>. if +n+ is passed, returns <tt>[]</tt>. + # + # [1,2,3,4,5,6].sample # => 4 + # [1,2,3,4,5,6].sample(3) # => [2, 4, 5] + # [].sample # => nil + # [].sample(3) # => [] def sample(n=nil) return self[Kernel.rand(size)] if n.nil? n = n.to_int diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb index f2ca9c7cc9..6f308a0d62 100644 --- a/activesupport/lib/active_support/core_ext/class.rb +++ b/activesupport/lib/active_support/core_ext/class.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/inheritable_attributes' require 'active_support/core_ext/class/delegating_attributes' diff --git a/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb b/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb index 4be7eaf476..7aff05dcdf 100644 --- a/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb @@ -128,7 +128,7 @@ class Class # :nodoc: private # Prevent this constant from being created multiple times - EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze unless const_defined?(:EMPTY_INHERITABLE_ATTRIBUTES) + EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze def inherited_with_inheritable_attributes(child) inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes) diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index bbd8f5aef6..7d58a8b56a 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -11,9 +11,9 @@ class Class #:nodoc: # Rubinius if defined?(Class.__subclasses__) - def descendents + def descendants subclasses = [] - __subclasses__.each {|k| subclasses << k; subclasses.concat k.descendents } + __subclasses__.each {|k| subclasses << k; subclasses.concat k.descendants } subclasses end else @@ -21,7 +21,7 @@ class Class #:nodoc: begin ObjectSpace.each_object(Class.new) {} - def descendents + def descendants subclasses = [] ObjectSpace.each_object(class << self; self; end) do |k| subclasses << k unless k == self @@ -30,7 +30,7 @@ class Class #:nodoc: end # JRuby rescue StandardError - def descendents + def descendants subclasses = [] ObjectSpace.each_object(Class) do |k| subclasses << k if k < self @@ -48,7 +48,7 @@ class Class #:nodoc: def self.subclasses_of(*superclasses) #:nodoc: subclasses = [] superclasses.each do |klass| - subclasses.concat klass.descendents.select {|k| k.anonymous? || k.reachable?} + subclasses.concat klass.descendants.select {|k| k.anonymous? || k.reachable?} end subclasses end diff --git a/activesupport/lib/active_support/core_ext/logger.rb b/activesupport/lib/active_support/core_ext/logger.rb index c4994ca2ee..d023b4bcff 100644 --- a/activesupport/lib/active_support/core_ext/logger.rb +++ b/activesupport/lib/active_support/core_ext/logger.rb @@ -72,44 +72,6 @@ class Logger @formatter ||= SimpleFormatter.new end - unless const_defined? :Formatter - class Formatter - Format = "%s, [%s#%d] %5s -- %s: %s\n" - - attr_accessor :datetime_format - - def initialize - @datetime_format = nil - end - - def call(severity, time, progname, msg) - Format % [severity[0..0], format_datetime(time), $$, severity, progname, - msg2str(msg)] - end - - private - def format_datetime(time) - if @datetime_format.nil? - time.strftime("%Y-%m-%dT%H:%M:%S.") << "%06d " % time.usec - else - time.strftime(@datetime_format) - end - end - - def msg2str(msg) - case msg - when ::String - msg - when ::Exception - "#{ msg.message } (#{ msg.class })\n" << - (msg.backtrace || []).join("\n") - else - msg.inspect - end - end - end - end - # Simple formatter which only displays the message. class SimpleFormatter < Logger::Formatter # This method is invoked when a log event occurs diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb index 06f077e920..06f077e920 100755..100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb diff --git a/activesupport/lib/active_support/core_ext/range/overlaps.rb b/activesupport/lib/active_support/core_ext/range/overlaps.rb index 0dec6e0ac4..7df653b53f 100644 --- a/activesupport/lib/active_support/core_ext/range/overlaps.rb +++ b/activesupport/lib/active_support/core_ext/range/overlaps.rb @@ -1,5 +1,5 @@ class Range - # Compare two ranges and see if they overlap eachother + # Compare two ranges and see if they overlap each other # (1..5).overlaps?(4..6) # => true # (1..5).overlaps?(7..9) # => false def overlaps?(other) diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index cd7a42ed2b..5b2cb6e331 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -28,6 +28,9 @@ class String self[0] end unless method_defined?(:ord) + # +getbyte+ backport from Ruby 1.9 + alias_method :getbyte, :[] unless method_defined?(:getbyte) + # Form can be either :utc (default) or :local. def to_time(form = :utc) return nil if self.blank? @@ -47,15 +50,4 @@ class String d[5] += d.pop ::DateTime.civil(*d) end - - # +constantize+ tries to find a declared constant with the name specified - # in the string. It raises a NameError when the name is not in CamelCase - # or is not initialized. - # - # Examples - # "Module".constantize # => Module - # "Class".constantize # => Class - def constantize - ActiveSupport::Inflector.constantize(self) - end end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index ef479d559e..66c4034781 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -28,6 +28,17 @@ class String ActiveSupport::Inflector.singularize(self) end + # +constantize+ tries to find a declared constant with the name specified + # in the string. It raises a NameError when the name is not in CamelCase + # or is not initialized. + # + # Examples + # "Module".constantize # => Module + # "Class".constantize # => Class + def constantize + ActiveSupport::Inflector.constantize(self) + end + # By default, +camelize+ converts strings to UpperCamelCase. If the argument to camelize # is set to <tt>:lower</tt> then camelize produces lowerCamelCase. # diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 2cc812643f..7d5143ba37 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -33,14 +33,14 @@ module ActiveSupport #:nodoc: # The set of directories from which we may automatically load files. Files # under these directories will be reloaded on each request in development mode, - # unless the directory also appears in load_once_paths. - mattr_accessor :load_paths - self.load_paths = [] + # unless the directory also appears in autoload_once_paths. + mattr_accessor :autoload_paths + self.autoload_paths = [] # The set of directories from which automatically loaded constants are loaded - # only once. All directories in this set must also be present in +load_paths+. - mattr_accessor :load_once_paths - self.load_once_paths = [] + # only once. All directories in this set must also be present in +autoload_paths+. + mattr_accessor :autoload_once_paths + self.autoload_once_paths = [] # An array of qualified constant names that have been loaded. Adding a name to # this array will cause it to be unloaded the next time Dependencies are cleared. @@ -340,7 +340,7 @@ module ActiveSupport #:nodoc: if Module.method(:const_defined?).arity == 1 # Does this module define this constant? - # Wrapper to accomodate changing Module#const_defined? in Ruby 1.9 + # Wrapper to accommodate changing Module#const_defined? in Ruby 1.9 def local_const_defined?(mod, const) mod.const_defined?(const) end @@ -352,22 +352,31 @@ module ActiveSupport #:nodoc: # Given +path+, a filesystem path to a ruby file, return an array of constant # paths which would cause Dependencies to attempt to load this file. - def loadable_constants_for_path(path, bases = load_paths) - expanded_path = Pathname.new(path[/\A(.*?)(\.rb)?\Z/, 1]).expand_path + def loadable_constants_for_path(path, bases = autoload_paths) + path = $1 if path =~ /\A(.*)\.rb\Z/ + expanded_path = File.expand_path(path) + paths = [] + + bases.each do |root| + expanded_root = File.expand_path(root) + next unless %r{\A#{Regexp.escape(expanded_root)}(/|\\)} =~ expanded_path + + nesting = expanded_path[(expanded_root.size)..-1] + nesting = nesting[1..-1] if nesting && nesting[0] == ?/ + next if nesting.blank? - bases.inject([]) do |paths, root| - expanded_root = Pathname.new(root).expand_path - nesting = expanded_path.relative_path_from(expanded_root).to_s - next paths if nesting =~ /\.\./ paths << nesting.camelize - end.uniq + end + + paths.uniq! + paths end - # Search for a file in load_paths matching the provided suffix. + # Search for a file in autoload_paths matching the provided suffix. def search_for_file(path_suffix) path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb") - load_paths.each do |root| + autoload_paths.each do |root| path = File.join(root, path_suffix) return path if File.file? path end @@ -377,14 +386,14 @@ module ActiveSupport #:nodoc: # Does the provided path_suffix correspond to an autoloadable module? # Instead of returning a boolean, the autoload base for this module is returned. def autoloadable_module?(path_suffix) - load_paths.each do |load_path| + autoload_paths.each do |load_path| return load_path if File.directory? File.join(load_path, path_suffix) end nil end def load_once_path?(path) - load_once_paths.any? { |base| path.starts_with? base } + autoload_once_paths.any? { |base| path.starts_with? base } end # Attempt to autoload the provided module name by searching for a directory @@ -396,7 +405,7 @@ module ActiveSupport #:nodoc: return nil unless base_path = autoloadable_module?(path_suffix) mod = Module.new into.const_set const_name, mod - autoloaded_constants << qualified_name unless load_once_paths.include?(base_path) + autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path) return mod end diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 578c025fcf..feb1508586 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -1,28 +1,27 @@ +require "active_support/notifications" + module ActiveSupport module Deprecation class << self - # Behavior is a block that takes a message argument. - attr_writer :behavior - # Whether to print a backtrace along with the warning. attr_accessor :debug def behavior - @behavior ||= default_behavior + @behavior ||= DEFAULT_BEHAVIORS[:stderr] end - def default_behavior - Deprecation::DEFAULT_BEHAVIORS[defined?(Rails.env) ? Rails.env.to_s : 'test'] + def behavior=(behavior) + @behavior = DEFAULT_BEHAVIORS[behavior] || behavior end end - # Default warning behaviors per Rails.env. Ignored in production. + # Default warning behaviors per Rails.env. DEFAULT_BEHAVIORS = { - 'test' => Proc.new { |message, callstack| + :stderr => Proc.new { |message, callstack| $stderr.puts(message) $stderr.puts callstack.join("\n ") if debug }, - 'development' => Proc.new { |message, callstack| + :log => Proc.new { |message, callstack| logger = if defined?(Rails) && Rails.logger Rails.logger @@ -32,6 +31,10 @@ module ActiveSupport end logger.warn message logger.debug callstack.join("\n ") if debug + }, + :notify => Proc.new { |message, callstack| + ActiveSupport::Notifications.instrument("deprecation.rails", + :message => message, :callstack => callstack) } } end diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb new file mode 100644 index 0000000000..a587d7770c --- /dev/null +++ b/activesupport/lib/active_support/descendants_tracker.rb @@ -0,0 +1,39 @@ +require 'active_support/dependencies' + +module ActiveSupport + # This module provides an internal implementation to track descendants + # which is faster than iterating through ObjectSpace. + module DescendantsTracker + @@descendants = Hash.new { |h, k| h[k] = [] } + + def self.descendants + @@descendants + end + + def self.clear + @@descendants.each do |klass, descendants| + if ActiveSupport::Dependencies.autoloaded?(klass) + @@descendants.delete(klass) + else + descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) } + end + end + end + + def inherited(base) + self.direct_descendants << base + super + end + + def direct_descendants + @@descendants[self] + end + + def descendants + @@descendants[self].inject([]) do |descendants, klass| + descendants << klass + descendants.concat klass.descendants + end + end + end +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb new file mode 100644 index 0000000000..5f5b264eb9 --- /dev/null +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -0,0 +1,37 @@ +module ActiveSupport + # This class is responsible to track files and invoke the given block + # whenever one of these files are changed. For example, this class + # is used by Rails to reload routes whenever they are changed upon + # a new request. + # + # routes_reloader = ActiveSupport::FileUpdateChecker.new(paths) do + # paths.each { |p| load(p) } + # Rails::Application.routes.reload! + # end + # + # ActionDispatch::Callbacks.to_prepare do + # routes_reloader.execute_if_updated + # end + # + class FileUpdateChecker + attr_reader :paths, :last_update_at + + def initialize(paths, calculate=false, &block) + @paths = paths + @block = block + @last_update_at = calculate ? updated_at : nil + end + + def updated_at + paths.map { |path| File.stat(path).mtime }.max + end + + def execute_if_updated + current_update_at = self.updated_at + if @last_update_at != current_update_at + @last_update_at = current_update_at + @block.call + end + end + end +end diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index 0ffdd904fd..45b9d20c01 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -4,5 +4,6 @@ rescue LoadError => e $stderr.puts "You don't have i18n installed in your application. Please add it to your Gemfile and run bundle install" raise e end + I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml" ActiveSupport.run_load_hooks(:i18n) diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb new file mode 100644 index 0000000000..d82e54f1d4 --- /dev/null +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -0,0 +1,80 @@ +require "active_support" +require "rails" +require "active_support/file_update_checker" + +module I18n + class Railtie < Rails::Railtie + config.i18n = ActiveSupport::OrderedOptions.new + config.i18n.railties_load_path = [] + config.i18n.load_path = [] + config.i18n.fallbacks = ActiveSupport::OrderedOptions.new + + def self.reloader + @reloader ||= ActiveSupport::FileUpdateChecker.new([]){ I18n.reload! } + end + + # Add I18n::Railtie.reloader to ActionDispatch callbacks. Since, at this + # point, no path was added to the reloader, I18n.reload! is not triggered + # on to_prepare callbacks. This will only happen on the config.after_initialize + # callback below. + initializer "i18n.callbacks" do + ActionDispatch::Callbacks.to_prepare do + I18n::Railtie.reloader.execute_if_updated + end + end + + # Set the i18n configuration only after initialization since a lot of + # configuration is still usually done in application initializers. + config.after_initialize do |app| + fallbacks = app.config.i18n.delete(:fallbacks) + + app.config.i18n.each do |setting, value| + case setting + when :railties_load_path + app.config.i18n.load_path.unshift(*value) + when :load_path + I18n.load_path += value + else + I18n.send("#{setting}=", value) + end + end + + init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks) + + reloader.paths.concat I18n.load_path + reloader.execute_if_updated + end + + protected + + def self.include_fallbacks_module + I18n.backend.class.send(:include, I18n::Backend::Fallbacks) + end + + def self.init_fallbacks(fallbacks) + include_fallbacks_module + + args = case fallbacks + when ActiveSupport::OrderedOptions + [*(fallbacks[:defaults] || []) << fallbacks[:map]].compact + when Hash, Array + Array.wrap(fallbacks) + else # TrueClass + [] + end + + I18n.fallbacks = I18n::Locale::Fallbacks.new(*args) + end + + def self.validate_fallbacks(fallbacks) + case fallbacks + when ActiveSupport::OrderedOptions + !fallbacks.empty? + when TrueClass, Array, Hash + true + else + raise "Unexpected fallback type #{fallbacks.inspect}" + end + end + end +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/json/backends/yajl.rb b/activesupport/lib/active_support/json/backends/yajl.rb index d76f8b03e4..64e50e0d87 100644 --- a/activesupport/lib/active_support/json/backends/yajl.rb +++ b/activesupport/lib/active_support/json/backends/yajl.rb @@ -1,4 +1,4 @@ -require 'yajl-ruby' unless defined?(Yajl) +require 'yajl' unless defined?(Yajl) module ActiveSupport module JSON diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 02c233595d..dd94315111 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -128,12 +128,21 @@ module ActiveSupport end end -class Object - # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. - def to_json(options = nil) - ActiveSupport::JSON.encode(self, options) - end +# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting +# their default behavior. That said, we need to define the basic to_json method in all of them, +# otherwise they will always use to_json gem implementation, which is backwards incompatible in +# several cases (for instance, the JSON implementation for Hash does not work) with inheritance +# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| + klass.class_eval <<-RUBY, __FILE__, __LINE__ + # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. + def to_json(options = nil) + ActiveSupport::JSON.encode(self, options) + end + RUBY +end +class Object def as_json(options = nil) #:nodoc: if respond_to?(:to_hash) to_hash diff --git a/activesupport/lib/active_support/locale/en.yml b/activesupport/lib/active_support/locale/en.yml index 49ad192bf1..8f08f37a70 100644 --- a/activesupport/lib/active_support/locale/en.yml +++ b/activesupport/lib/active_support/locale/en.yml @@ -14,7 +14,7 @@ en: # Don't forget the nil at the beginning; there's no such thing as a 0th month month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] - # Used in date_select and datime_select. + # Used in date_select and datetime_select. order: - :year - :month diff --git a/railties/lib/rails/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index a30701d4d5..891d718af3 100644 --- a/railties/lib/rails/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -1,58 +1,66 @@ -require 'active_support/core_ext/class/inheritable_attributes' -require 'active_support/notifications' +require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/core_ext/class/attribute' -module Rails - # Rails::LogSubscriber is an object set to consume ActiveSupport::Notifications - # on initialization with solely purpose of logging. The log subscriber dispatches - # notifications to a regirested object based on its given namespace. +module ActiveSupport + # ActiveSupport::LogSubscriber is an object set to consume ActiveSupport::Notifications + # with solely purpose of logging. The log subscriber dispatches notifications to a + # regirested object based on its given namespace. # # An example would be Active Record log subscriber responsible for logging queries: # # module ActiveRecord - # class Railtie - # class LogSubscriber < Rails::LogSubscriber - # def sql(event) - # "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}" - # end + # class LogSubscriber < ActiveSupport::LogSubscriber + # def sql(event) + # "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}" # end # end # end # - # It's finally registed as: + # And it's finally registed as: # - # Rails::LogSubscriber.add :active_record, ActiveRecord::Railtie::LogSubscriber.new + # ActiveRecord::LogSubscriber.attach_to :active_record # - # So whenever a "sql.active_record" notification arrive to Rails::LogSubscriber, + # Since we need to know all instance methods before attaching the log subscriber, + # the line above shuold be called after your ActiveRecord::LogSubscriber definition. + # + # After configured, whenever a "sql.active_record" notification is published, # it will properly dispatch the event (ActiveSupport::Notifications::Event) to # the sql method. # - # This is useful because it avoids spanning several log subscribers just for logging - # purposes(which slows down the main thread). Besides of providing a centralized - # facility on top of Rails.logger. - # # Log subscriber also has some helpers to deal with logging and automatically flushes - # all logs when the request finishes (via action_dispatch.callback notification). + # all logs when the request finishes (via action_dispatch.callback notification) in + # a Rails environment. class LogSubscriber mattr_accessor :colorize_logging self.colorize_logging = true - # Embed in a String to clear all previous ANSI sequences. - CLEAR = "\e[0m" - BOLD = "\e[1m" + class_attribute :logger + + class << self + remove_method :logger + end + def self.logger + @logger ||= Rails.logger if defined?(Rails) + end + + # Embed in a String to clear all previous ANSI sequences. + CLEAR = "\e[0m" + BOLD = "\e[1m" + # Colors - BLACK = "\e[30m" - RED = "\e[31m" - GREEN = "\e[32m" - YELLOW = "\e[33m" - BLUE = "\e[34m" - MAGENTA = "\e[35m" - CYAN = "\e[36m" - WHITE = "\e[37m" + BLACK = "\e[30m" + RED = "\e[31m" + GREEN = "\e[32m" + YELLOW = "\e[33m" + BLUE = "\e[34m" + MAGENTA = "\e[35m" + CYAN = "\e[36m" + WHITE = "\e[37m" - def self.add(namespace, log_subscriber, notifier = ActiveSupport::Notifications) + def self.attach_to(namespace, log_subscriber=new, notifier=ActiveSupport::Notifications) log_subscribers << log_subscriber - @flushable_loggers = nil + @@flushable_loggers = nil log_subscriber.public_methods(false).each do |event| notifier.subscribe("#{event}.#{namespace}") do |*args| @@ -61,18 +69,18 @@ module Rails begin log_subscriber.send(event, ActiveSupport::Notifications::Event.new(*args)) rescue Exception => e - Rails.logger.error "Could not log #{args[0].inspect} event. #{e.class}: #{e.message}" + log_subscriber.logger.error "Could not log #{args[0].inspect} event. #{e.class}: #{e.message}" end end end end def self.log_subscribers - @log_subscribers ||= [] + @@log_subscribers ||= [] end def self.flushable_loggers - @flushable_loggers ||= begin + @@flushable_loggers ||= begin loggers = log_subscribers.map(&:logger) loggers.uniq! loggers.select { |l| l.respond_to?(:flush) } @@ -84,11 +92,6 @@ module Rails flushable_loggers.each(&:flush) end - # By default, we use the Rails.logger for logging. - def logger - Rails.logger - end - protected %w(info debug warn error fatal unknown).each do |level| diff --git a/railties/lib/rails/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb index 9b7b0738cd..96506a4b2b 100644 --- a/railties/lib/rails/log_subscriber/test_helper.rb +++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb @@ -1,13 +1,16 @@ -require 'rails/log_subscriber' +require 'active_support/log_subscriber' -module Rails +module ActiveSupport class LogSubscriber # Provides some helpers to deal with testing log subscribers by setting up # notifications. Take for instance Active Record subscriber tests: # # class SyncLogSubscriberTest < ActiveSupport::TestCase - # include Rails::LogSubscriber::TestHelper - # Rails::LogSubscriber.add(:active_record, ActiveRecord::Railties::LogSubscriber.new) + # include ActiveSupport::LogSubscriber::TestHelper + # + # def setup + # ActiveRecord::LogSubscriber.attach_to(:active_record) + # end # # def test_basic_query_logging # Developer.all @@ -16,16 +19,6 @@ module Rails # assert_match /Developer Load/, @logger.logged(:debug).last # assert_match /SELECT \* FROM "developers"/, @logger.logged(:debug).last # end - # - # class SyncLogSubscriberTest < ActiveSupport::TestCase - # include Rails::LogSubscriber::SyncTestHelper - # include LogSubscriberTest - # end - # - # class AsyncLogSubscriberTest < ActiveSupport::TestCase - # include Rails::LogSubscriber::AsyncTestHelper - # include LogSubscriberTest - # end # end # # All you need to do is to ensure that your log subscriber is added to Rails::Subscriber, @@ -42,7 +35,7 @@ module Rails @logger = MockLogger.new @notifier = ActiveSupport::Notifications::Notifier.new(queue) - Rails::LogSubscriber.colorize_logging = false + ActiveSupport::LogSubscriber.colorize_logging = false set_logger(@logger) ActiveSupport::Notifications.notifier = @notifier @@ -86,7 +79,7 @@ module Rails # end # def set_logger(logger) - Rails.logger = logger + ActiveSupport::LogSubscriber.logger = logger end def queue diff --git a/activesupport/lib/active_support/multibyte.rb b/activesupport/lib/active_support/multibyte.rb index e7a271a660..8ffdf5a1bf 100644 --- a/activesupport/lib/active_support/multibyte.rb +++ b/activesupport/lib/active_support/multibyte.rb @@ -17,7 +17,7 @@ module ActiveSupport #:nodoc: @proxy_class = klass end - # Returns the currect proxy class + # Returns the current proxy class def self.proxy_class @proxy_class ||= ActiveSupport::Multibyte::Chars end diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index d6ccb4bac1..8823e4a5ed 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -50,10 +50,6 @@ module ActiveSupport #:nodoc: end end - def <=>(other) - @wrapped_string <=> other - end - # Forward all undefined methods to the wrapped string. def method_missing(method, *args, &block) if method.to_s =~ /!$/ @@ -87,6 +83,16 @@ module ActiveSupport #:nodoc: include Comparable + # Returns <tt>-1</tt>, <tt>0</tt> or <tt>+1</tt> depending on whether the Chars object is to be sorted before, + # equal or after the object on the right side of the operation. It accepts any object that implements +to_s+. + # See <tt>String#<=></tt> for more details. + # + # Example: + # 'é'.mb_chars <=> 'ü'.mb_chars #=> -1 + def <=>(other) + @wrapped_string <=> other.to_s + end + if RUBY_VERSION < "1.9" # Returns +true+ if the Chars class can and should act as a proxy for the string _string_. Returns # +false+ otherwise. @@ -94,22 +100,12 @@ module ActiveSupport #:nodoc: $KCODE == 'UTF8' && consumes?(string) end - # Returns <tt>-1</tt>, <tt>0</tt> or <tt>+1</tt> depending on whether the Chars object is to be sorted before, - # equal or after the object on the right side of the operation. It accepts any object that implements +to_s+. - # See <tt>String#<=></tt> for more details. - # - # Example: - # 'é'.mb_chars <=> 'ü'.mb_chars #=> -1 - def <=>(other) - @wrapped_string <=> other.to_s - end - # Returns a new Chars object containing the _other_ object concatenated to the string. # # Example: # ('Café'.mb_chars + ' périferôl').to_s #=> "Café périferôl" def +(other) - self << other + chars(@wrapped_string + other) end # Like <tt>String#=~</tt> only it returns the character offset (in codepoints) instead of the byte offset. @@ -199,6 +195,45 @@ module ActiveSupport #:nodoc: Unicode.u_unpack(@wrapped_string)[0] end + # Works just like <tt>String#rjust</tt>, only integer specifies characters instead of bytes. + # + # Example: + # + # "¾ cup".mb_chars.rjust(8).to_s + # #=> " ¾ cup" + # + # "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace + # #=> " ¾ cup" + def rjust(integer, padstr=' ') + justify(integer, :right, padstr) + end + + # Works just like <tt>String#ljust</tt>, only integer specifies characters instead of bytes. + # + # Example: + # + # "¾ cup".mb_chars.rjust(8).to_s + # #=> "¾ cup " + # + # "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace + # #=> "¾ cup " + def ljust(integer, padstr=' ') + justify(integer, :left, padstr) + end + + # Works just like <tt>String#center</tt>, only integer specifies characters instead of bytes. + # + # Example: + # + # "¾ cup".mb_chars.center(8).to_s + # #=> " ¾ cup " + # + # "¾ cup".mb_chars.center(8, " ").to_s # Use non-breaking whitespace + # #=> " ¾ cup " + def center(integer, padstr=' ') + justify(integer, :center, padstr) + end + else def =~(other) @wrapped_string =~ other @@ -254,46 +289,6 @@ module ActiveSupport #:nodoc: end end - # Works just like <tt>String#rjust</tt>, only integer specifies characters instead of bytes. - # - # Example: - # - # "¾ cup".mb_chars.rjust(8).to_s - # #=> " ¾ cup" - # - # "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace - # #=> " ¾ cup" - def rjust(integer, padstr=' ') - justify(integer, :right, padstr) - end - - # Works just like <tt>String#ljust</tt>, only integer specifies characters instead of bytes. - # - # Example: - # - # "¾ cup".mb_chars.rjust(8).to_s - # #=> "¾ cup " - # - # "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace - # #=> "¾ cup " - def ljust(integer, padstr=' ') - justify(integer, :left, padstr) - end - - # Works just like <tt>String#center</tt>, only integer specifies characters instead of bytes. - # - # Example: - # - # "¾ cup".mb_chars.center(8).to_s - # #=> " ¾ cup " - # - # "¾ cup".mb_chars.center(8, " ").to_s # Use non-breaking whitespace - # #=> " ¾ cup " - def center(integer, padstr=' ') - justify(integer, :center, padstr) - end - - # Reverses all characters in the string. # # Example: @@ -321,11 +316,12 @@ module ActiveSupport #:nodoc: result = @wrapped_string.slice(*args) elsif args.size == 1 && args[0].kind_of?(Numeric) character = Unicode.u_unpack(@wrapped_string)[args[0]] - result = character.nil? ? nil : [character].pack('U') + result = character && [character].pack('U') else - result = Unicode.u_unpack(@wrapped_string).slice(*args).pack('U*') + cps = Unicode.u_unpack(@wrapped_string).slice(*args) + result = cps && cps.pack('U*') end - result.nil? ? nil : chars(result) + result && chars(result) end alias_method :[], :slice @@ -375,6 +371,16 @@ module ActiveSupport #:nodoc: (slice(0) || chars('')).upcase + (slice(1..-1) || chars('')).downcase end + # Capitalizes the first letter of every word, when possible. + # + # Example: + # "ÉL QUE SE ENTERÓ".mb_chars.titleize # => "Él Que Se Enteró" + # "日本語".mb_chars.titleize # => "日本語" + def titleize + chars(downcase.to_s.gsub(/\b('?[\S])/u) { Unicode.apply_mapping $1, :uppercase_mapping }) + end + alias_method :titlecase, :titleize + # Returns the KC normalization of the string by default. NFKC is considered the best normalization form for # passing strings to databases and validations. # diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index f91e50c755..11c72d873b 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -9,7 +9,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = '5.1.0' + UNICODE_VERSION = '5.2.0' # The default normalization used for operations that require normalization. It can be set to any of the # normalizations in NORMALIZATION_FORMS. diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 3f1fe64e9b..1444fc1609 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -23,8 +23,7 @@ module ActiveSupport # # event = @events.first # event.name #=> :render - # event.duration #=> 10 (in miliseconds) - # event.result #=> "Foo" + # event.duration #=> 10 (in milliseconds) # event.payload #=> { :extra => :information } # # When subscribing to Notifications, you can pass a pattern, to only consume diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 91de722748..6b563b9063 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -149,6 +149,8 @@ module ActiveSupport self end + alias_method :update, :merge! + def merge(other_hash, &block) dup.merge!(other_hash, &block) end @@ -160,6 +162,10 @@ module ActiveSupport self end + def invert + OrderedHash[self.to_a.map!{|key_value_pair| key_value_pair.reverse}] + end + def inspect "#<OrderedHash #{super}>" end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 59f9ab18b1..c2deba3b1b 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -1,5 +1,6 @@ require "active_support" require "rails" +require "active_support/i18n_railtie" module ActiveSupport class Railtie < Rails::Railtie @@ -11,6 +12,36 @@ module ActiveSupport require 'active_support/whiny_nil' if app.config.whiny_nils end + initializer "active_support.deprecation_behavior" do |app| + if deprecation = app.config.active_support.deprecation + ActiveSupport::Deprecation.behavior = deprecation + else + defaults = {"development" => :log, + "production" => :notify, + "test" => :stderr} + + env = Rails.env + + if defaults.key?(env) + msg = "You did not specify how you would like Rails to report " \ + "deprecation notices for your #{env} environment, please " \ + "set config.active_support.deprecation to :#{defaults[env]} " \ + "at config/environments/#{env}.rb" + + warn msg + ActiveSupport::Deprecation.behavior = defaults[env] + else + msg = "You did not specify how you would like Rails to report " \ + "deprecation notices for your #{env} environment, please " \ + "set config.active_support.deprecation to :log, :notify or " \ + ":stderr at config/environments/#{env}.rb" + + warn msg + ActiveSupport::Deprecation.behavior = :stderr + end + end + end + # Sets the default value for Time.zone # If assigned value cannot be matched to a TimeZone, an exception will be raised. initializer "active_support.initialize_time_zone" do |app| @@ -27,74 +58,3 @@ module ActiveSupport end end end - -module I18n - class Railtie < Rails::Railtie - config.i18n = ActiveSupport::OrderedOptions.new - config.i18n.railties_load_path = [] - config.i18n.load_path = [] - config.i18n.fallbacks = ActiveSupport::OrderedOptions.new - - initializer "i18n.initialize" do - ActiveSupport.on_load(:i18n) do - I18n.reload! - - ActionDispatch::Callbacks.to_prepare do - I18n.reload! - end - end - end - - # Set the i18n configuration from config.i18n but special-case for - # the load_path which should be appended to what's already set instead of overwritten. - config.after_initialize do |app| - fallbacks = app.config.i18n.delete(:fallbacks) - - app.config.i18n.each do |setting, value| - case setting - when :railties_load_path - app.config.i18n.load_path.unshift(*value) - when :load_path - I18n.load_path += value - else - I18n.send("#{setting}=", value) - end - end - - init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks) - I18n.reload! - end - - class << self - protected - - def init_fallbacks(fallbacks) - include_fallbacks_module - args = case fallbacks - when ActiveSupport::OrderedOptions - [*(fallbacks[:defaults] || []) << fallbacks[:map]].compact - when Hash, Array - Array.wrap(fallbacks) - else # TrueClass - [] - end - I18n.fallbacks = I18n::Locale::Fallbacks.new(*args) - end - - def include_fallbacks_module - I18n.backend.class.send(:include, I18n::Backend::Fallbacks) - end - - def validate_fallbacks(fallbacks) - case fallbacks - when ActiveSupport::OrderedOptions - !fallbacks.empty? - when TrueClass, Array, Hash - true - else - raise "Unexpected fallback type #{fallbacks.inspect}" - end - end - end - end -end
\ No newline at end of file diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 69df399cde..d629f6f2b7 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -45,12 +45,16 @@ module ActiveSupport end end + def _run_class_setup # class setup method should only happen in parent + unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST'] + self.class.setup if self.class.respond_to?(:setup) + @@ran_class_setup = true + end + end + module TestUnit def run(result) - unless defined?(@@ran_class_setup) - self.class.setup if self.class.respond_to?(:setup) - @@ran_class_setup = true - end + _run_class_setup yield(Test::Unit::TestCase::STARTED, name) @@ -74,10 +78,7 @@ module ActiveSupport module MiniTest def run(runner) - unless defined?(@@ran_class_setup) - self.class.setup if self.class.respond_to?(:setup) - @@ran_class_setup = true - end + _run_class_setup serialized = run_in_isolation do |isolated_runner| super(isolated_runner) @@ -109,6 +110,8 @@ module ActiveSupport end module Subprocess + ORIG_ARGV = ARGV.dup unless defined?(ORIG_ARGV) + # Crazy H4X to get this working in windows / jruby with # no forking. def run_in_isolation(&blk) diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 710dce78de..62d02bdeb6 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -18,7 +18,7 @@ module ActiveSupport # # See Time and TimeZone for further documentation of these methods. # - # TimeWithZone instances implement the same API as Ruby Time instances, so that Time and TimeWithZone instances are interchangable. Examples: + # TimeWithZone instances implement the same API as Ruby Time instances, so that Time and TimeWithZone instances are interchangeable. Examples: # # t = Time.zone.now # => Sun, 18 May 2008 13:27:25 EDT -04:00 # t.hour # => 13 diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 67b37785f5..05b40298d3 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -30,154 +30,152 @@ end # (if a recent version of the gem is installed locally, this will be used instead of the bundled version.) module ActiveSupport class TimeZone - unless const_defined?(:MAPPING) - # Keys are Rails TimeZone names, values are TZInfo identifiers - MAPPING = { - "International Date Line West" => "Pacific/Midway", - "Midway Island" => "Pacific/Midway", - "Samoa" => "Pacific/Pago_Pago", - "Hawaii" => "Pacific/Honolulu", - "Alaska" => "America/Juneau", - "Pacific Time (US & Canada)" => "America/Los_Angeles", - "Tijuana" => "America/Tijuana", - "Mountain Time (US & Canada)" => "America/Denver", - "Arizona" => "America/Phoenix", - "Chihuahua" => "America/Chihuahua", - "Mazatlan" => "America/Mazatlan", - "Central Time (US & Canada)" => "America/Chicago", - "Saskatchewan" => "America/Regina", - "Guadalajara" => "America/Mexico_City", - "Mexico City" => "America/Mexico_City", - "Monterrey" => "America/Monterrey", - "Central America" => "America/Guatemala", - "Eastern Time (US & Canada)" => "America/New_York", - "Indiana (East)" => "America/Indiana/Indianapolis", - "Bogota" => "America/Bogota", - "Lima" => "America/Lima", - "Quito" => "America/Lima", - "Atlantic Time (Canada)" => "America/Halifax", - "Caracas" => "America/Caracas", - "La Paz" => "America/La_Paz", - "Santiago" => "America/Santiago", - "Newfoundland" => "America/St_Johns", - "Brasilia" => "America/Sao_Paulo", - "Buenos Aires" => "America/Argentina/Buenos_Aires", - "Georgetown" => "America/Guyana", - "Greenland" => "America/Godthab", - "Mid-Atlantic" => "Atlantic/South_Georgia", - "Azores" => "Atlantic/Azores", - "Cape Verde Is." => "Atlantic/Cape_Verde", - "Dublin" => "Europe/Dublin", - "Edinburgh" => "Europe/London", - "Lisbon" => "Europe/Lisbon", - "London" => "Europe/London", - "Casablanca" => "Africa/Casablanca", - "Monrovia" => "Africa/Monrovia", - "UTC" => "Etc/UTC", - "Belgrade" => "Europe/Belgrade", - "Bratislava" => "Europe/Bratislava", - "Budapest" => "Europe/Budapest", - "Ljubljana" => "Europe/Ljubljana", - "Prague" => "Europe/Prague", - "Sarajevo" => "Europe/Sarajevo", - "Skopje" => "Europe/Skopje", - "Warsaw" => "Europe/Warsaw", - "Zagreb" => "Europe/Zagreb", - "Brussels" => "Europe/Brussels", - "Copenhagen" => "Europe/Copenhagen", - "Madrid" => "Europe/Madrid", - "Paris" => "Europe/Paris", - "Amsterdam" => "Europe/Amsterdam", - "Berlin" => "Europe/Berlin", - "Bern" => "Europe/Berlin", - "Rome" => "Europe/Rome", - "Stockholm" => "Europe/Stockholm", - "Vienna" => "Europe/Vienna", - "West Central Africa" => "Africa/Algiers", - "Bucharest" => "Europe/Bucharest", - "Cairo" => "Africa/Cairo", - "Helsinki" => "Europe/Helsinki", - "Kyiv" => "Europe/Kiev", - "Riga" => "Europe/Riga", - "Sofia" => "Europe/Sofia", - "Tallinn" => "Europe/Tallinn", - "Vilnius" => "Europe/Vilnius", - "Athens" => "Europe/Athens", - "Istanbul" => "Europe/Istanbul", - "Minsk" => "Europe/Minsk", - "Jerusalem" => "Asia/Jerusalem", - "Harare" => "Africa/Harare", - "Pretoria" => "Africa/Johannesburg", - "Moscow" => "Europe/Moscow", - "St. Petersburg" => "Europe/Moscow", - "Volgograd" => "Europe/Moscow", - "Kuwait" => "Asia/Kuwait", - "Riyadh" => "Asia/Riyadh", - "Nairobi" => "Africa/Nairobi", - "Baghdad" => "Asia/Baghdad", - "Tehran" => "Asia/Tehran", - "Abu Dhabi" => "Asia/Muscat", - "Muscat" => "Asia/Muscat", - "Baku" => "Asia/Baku", - "Tbilisi" => "Asia/Tbilisi", - "Yerevan" => "Asia/Yerevan", - "Kabul" => "Asia/Kabul", - "Ekaterinburg" => "Asia/Yekaterinburg", - "Islamabad" => "Asia/Karachi", - "Karachi" => "Asia/Karachi", - "Tashkent" => "Asia/Tashkent", - "Chennai" => "Asia/Kolkata", - "Kolkata" => "Asia/Kolkata", - "Mumbai" => "Asia/Kolkata", - "New Delhi" => "Asia/Kolkata", - "Kathmandu" => "Asia/Kathmandu", - "Astana" => "Asia/Dhaka", - "Dhaka" => "Asia/Dhaka", - "Sri Jayawardenepura" => "Asia/Colombo", - "Almaty" => "Asia/Almaty", - "Novosibirsk" => "Asia/Novosibirsk", - "Rangoon" => "Asia/Rangoon", - "Bangkok" => "Asia/Bangkok", - "Hanoi" => "Asia/Bangkok", - "Jakarta" => "Asia/Jakarta", - "Krasnoyarsk" => "Asia/Krasnoyarsk", - "Beijing" => "Asia/Shanghai", - "Chongqing" => "Asia/Chongqing", - "Hong Kong" => "Asia/Hong_Kong", - "Urumqi" => "Asia/Urumqi", - "Kuala Lumpur" => "Asia/Kuala_Lumpur", - "Singapore" => "Asia/Singapore", - "Taipei" => "Asia/Taipei", - "Perth" => "Australia/Perth", - "Irkutsk" => "Asia/Irkutsk", - "Ulaan Bataar" => "Asia/Ulaanbaatar", - "Seoul" => "Asia/Seoul", - "Osaka" => "Asia/Tokyo", - "Sapporo" => "Asia/Tokyo", - "Tokyo" => "Asia/Tokyo", - "Yakutsk" => "Asia/Yakutsk", - "Darwin" => "Australia/Darwin", - "Adelaide" => "Australia/Adelaide", - "Canberra" => "Australia/Melbourne", - "Melbourne" => "Australia/Melbourne", - "Sydney" => "Australia/Sydney", - "Brisbane" => "Australia/Brisbane", - "Hobart" => "Australia/Hobart", - "Vladivostok" => "Asia/Vladivostok", - "Guam" => "Pacific/Guam", - "Port Moresby" => "Pacific/Port_Moresby", - "Magadan" => "Asia/Magadan", - "Solomon Is." => "Asia/Magadan", - "New Caledonia" => "Pacific/Noumea", - "Fiji" => "Pacific/Fiji", - "Kamchatka" => "Asia/Kamchatka", - "Marshall Is." => "Pacific/Majuro", - "Auckland" => "Pacific/Auckland", - "Wellington" => "Pacific/Auckland", - "Nuku'alofa" => "Pacific/Tongatapu" - }.each { |name, zone| name.freeze; zone.freeze } - MAPPING.freeze - end + # Keys are Rails TimeZone names, values are TZInfo identifiers + MAPPING = { + "International Date Line West" => "Pacific/Midway", + "Midway Island" => "Pacific/Midway", + "Samoa" => "Pacific/Pago_Pago", + "Hawaii" => "Pacific/Honolulu", + "Alaska" => "America/Juneau", + "Pacific Time (US & Canada)" => "America/Los_Angeles", + "Tijuana" => "America/Tijuana", + "Mountain Time (US & Canada)" => "America/Denver", + "Arizona" => "America/Phoenix", + "Chihuahua" => "America/Chihuahua", + "Mazatlan" => "America/Mazatlan", + "Central Time (US & Canada)" => "America/Chicago", + "Saskatchewan" => "America/Regina", + "Guadalajara" => "America/Mexico_City", + "Mexico City" => "America/Mexico_City", + "Monterrey" => "America/Monterrey", + "Central America" => "America/Guatemala", + "Eastern Time (US & Canada)" => "America/New_York", + "Indiana (East)" => "America/Indiana/Indianapolis", + "Bogota" => "America/Bogota", + "Lima" => "America/Lima", + "Quito" => "America/Lima", + "Atlantic Time (Canada)" => "America/Halifax", + "Caracas" => "America/Caracas", + "La Paz" => "America/La_Paz", + "Santiago" => "America/Santiago", + "Newfoundland" => "America/St_Johns", + "Brasilia" => "America/Sao_Paulo", + "Buenos Aires" => "America/Argentina/Buenos_Aires", + "Georgetown" => "America/Guyana", + "Greenland" => "America/Godthab", + "Mid-Atlantic" => "Atlantic/South_Georgia", + "Azores" => "Atlantic/Azores", + "Cape Verde Is." => "Atlantic/Cape_Verde", + "Dublin" => "Europe/Dublin", + "Edinburgh" => "Europe/London", + "Lisbon" => "Europe/Lisbon", + "London" => "Europe/London", + "Casablanca" => "Africa/Casablanca", + "Monrovia" => "Africa/Monrovia", + "UTC" => "Etc/UTC", + "Belgrade" => "Europe/Belgrade", + "Bratislava" => "Europe/Bratislava", + "Budapest" => "Europe/Budapest", + "Ljubljana" => "Europe/Ljubljana", + "Prague" => "Europe/Prague", + "Sarajevo" => "Europe/Sarajevo", + "Skopje" => "Europe/Skopje", + "Warsaw" => "Europe/Warsaw", + "Zagreb" => "Europe/Zagreb", + "Brussels" => "Europe/Brussels", + "Copenhagen" => "Europe/Copenhagen", + "Madrid" => "Europe/Madrid", + "Paris" => "Europe/Paris", + "Amsterdam" => "Europe/Amsterdam", + "Berlin" => "Europe/Berlin", + "Bern" => "Europe/Berlin", + "Rome" => "Europe/Rome", + "Stockholm" => "Europe/Stockholm", + "Vienna" => "Europe/Vienna", + "West Central Africa" => "Africa/Algiers", + "Bucharest" => "Europe/Bucharest", + "Cairo" => "Africa/Cairo", + "Helsinki" => "Europe/Helsinki", + "Kyiv" => "Europe/Kiev", + "Riga" => "Europe/Riga", + "Sofia" => "Europe/Sofia", + "Tallinn" => "Europe/Tallinn", + "Vilnius" => "Europe/Vilnius", + "Athens" => "Europe/Athens", + "Istanbul" => "Europe/Istanbul", + "Minsk" => "Europe/Minsk", + "Jerusalem" => "Asia/Jerusalem", + "Harare" => "Africa/Harare", + "Pretoria" => "Africa/Johannesburg", + "Moscow" => "Europe/Moscow", + "St. Petersburg" => "Europe/Moscow", + "Volgograd" => "Europe/Moscow", + "Kuwait" => "Asia/Kuwait", + "Riyadh" => "Asia/Riyadh", + "Nairobi" => "Africa/Nairobi", + "Baghdad" => "Asia/Baghdad", + "Tehran" => "Asia/Tehran", + "Abu Dhabi" => "Asia/Muscat", + "Muscat" => "Asia/Muscat", + "Baku" => "Asia/Baku", + "Tbilisi" => "Asia/Tbilisi", + "Yerevan" => "Asia/Yerevan", + "Kabul" => "Asia/Kabul", + "Ekaterinburg" => "Asia/Yekaterinburg", + "Islamabad" => "Asia/Karachi", + "Karachi" => "Asia/Karachi", + "Tashkent" => "Asia/Tashkent", + "Chennai" => "Asia/Kolkata", + "Kolkata" => "Asia/Kolkata", + "Mumbai" => "Asia/Kolkata", + "New Delhi" => "Asia/Kolkata", + "Kathmandu" => "Asia/Kathmandu", + "Astana" => "Asia/Dhaka", + "Dhaka" => "Asia/Dhaka", + "Sri Jayawardenepura" => "Asia/Colombo", + "Almaty" => "Asia/Almaty", + "Novosibirsk" => "Asia/Novosibirsk", + "Rangoon" => "Asia/Rangoon", + "Bangkok" => "Asia/Bangkok", + "Hanoi" => "Asia/Bangkok", + "Jakarta" => "Asia/Jakarta", + "Krasnoyarsk" => "Asia/Krasnoyarsk", + "Beijing" => "Asia/Shanghai", + "Chongqing" => "Asia/Chongqing", + "Hong Kong" => "Asia/Hong_Kong", + "Urumqi" => "Asia/Urumqi", + "Kuala Lumpur" => "Asia/Kuala_Lumpur", + "Singapore" => "Asia/Singapore", + "Taipei" => "Asia/Taipei", + "Perth" => "Australia/Perth", + "Irkutsk" => "Asia/Irkutsk", + "Ulaan Bataar" => "Asia/Ulaanbaatar", + "Seoul" => "Asia/Seoul", + "Osaka" => "Asia/Tokyo", + "Sapporo" => "Asia/Tokyo", + "Tokyo" => "Asia/Tokyo", + "Yakutsk" => "Asia/Yakutsk", + "Darwin" => "Australia/Darwin", + "Adelaide" => "Australia/Adelaide", + "Canberra" => "Australia/Melbourne", + "Melbourne" => "Australia/Melbourne", + "Sydney" => "Australia/Sydney", + "Brisbane" => "Australia/Brisbane", + "Hobart" => "Australia/Hobart", + "Vladivostok" => "Asia/Vladivostok", + "Guam" => "Pacific/Guam", + "Port Moresby" => "Pacific/Port_Moresby", + "Magadan" => "Asia/Magadan", + "Solomon Is." => "Asia/Magadan", + "New Caledonia" => "Pacific/Noumea", + "Fiji" => "Pacific/Fiji", + "Kamchatka" => "Asia/Kamchatka", + "Marshall Is." => "Pacific/Majuro", + "Auckland" => "Pacific/Auckland", + "Wellington" => "Pacific/Auckland", + "Nuku'alofa" => "Pacific/Tongatapu" + }.each { |name, zone| name.freeze; zone.freeze } + MAPPING.freeze UTC_OFFSET_WITH_COLON = '%s%02d:%02d' UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '') @@ -224,7 +222,7 @@ module ActiveSupport utc_offset == 0 && alternate_utc_string || self.class.seconds_to_utc_offset(utc_offset, colon) end - # Compare this time zone to the parameter. The two are comapred first on + # Compare this time zone to the parameter. The two are compared first on # their offsets, and then by name. def <=>(zone) result = (utc_offset <=> zone.utc_offset) @@ -352,7 +350,11 @@ module ActiveSupport def [](arg) case arg when String - zones_map[arg] ||= lookup(arg) + begin + zones_map[arg] ||= lookup(arg).tap { |tz| tz.utc_offset } + rescue TZInfo::InvalidTimezoneIdentifier + nil + end when Numeric, ActiveSupport::Duration arg *= 3600 if arg.abs <= 13 all.find { |z| z.utc_offset == arg.to_i } diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex 10f2cae465..4fe0268cca 100644 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ b/activesupport/lib/active_support/values/unicode_tables.dat diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index dd84860a91..6db21f9e12 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -1,3 +1,5 @@ +ORIG_ARGV = ARGV.dup + begin old, $VERBOSE = $VERBOSE, nil require File.expand_path('../../../load_paths', __FILE__) @@ -23,7 +25,7 @@ require 'active_support/ruby/shim' if RUBY_VERSION < '1.8.7' def uses_memcached(test_name) require 'memcache' begin - MemCache.new('localhost').stats + MemCache.new('localhost:11211').stats yield rescue MemCache::MemCacheError $stderr.puts "Skipping #{test_name} tests. Start memcached and try again." diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index d9702dd9ff..affa1b5e18 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -110,6 +110,14 @@ class StringInflectionsTest < Test::Unit::TestCase assert_equal 97, 'abc'.ord end + if RUBY_VERSION < '1.9' + def test_getbyte + assert_equal 97, 'a'.getbyte(0) + assert_equal 99, 'abc'.getbyte(2) + assert_nil 'abc'.getbyte(3) + end + end + def test_string_to_time assert_equal Time.utc(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time assert_equal Time.local(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time(:local) diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index cf11f4d28f..5ce4277672 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -832,9 +832,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase def test_time_zone_setter_with_invalid_zone Time.zone = 'foo' - assert_not_nil Time.zone - assert_equal 'foo', Time.zone.name - assert_raise(TZInfo::InvalidTimezoneIdentifier) { Time.zone.utc_offset } + assert_nil Time.zone Time.zone = -15.hours assert_nil Time.zone diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 5422c75f5f..c7088638c7 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -25,11 +25,11 @@ class DependenciesTest < Test::Unit::TestCase this_dir = File.dirname(__FILE__) parent_dir = File.dirname(this_dir) $LOAD_PATH.unshift(parent_dir) unless $LOAD_PATH.include?(parent_dir) - prior_load_paths = ActiveSupport::Dependencies.load_paths - ActiveSupport::Dependencies.load_paths = from.collect { |f| "#{this_dir}/#{f}" } + prior_autoload_paths = ActiveSupport::Dependencies.autoload_paths + ActiveSupport::Dependencies.autoload_paths = from.collect { |f| "#{this_dir}/#{f}" } yield ensure - ActiveSupport::Dependencies.load_paths = prior_load_paths + ActiveSupport::Dependencies.autoload_paths = prior_autoload_paths ActiveSupport::Dependencies.mechanism = old_mechanism ActiveSupport::Dependencies.explicitly_unloadable_constants = [] end @@ -264,7 +264,7 @@ class DependenciesTest < Test::Unit::TestCase def test_loadable_constants_for_path_should_provide_all_results fake_root = '/usr/apps/backpack' with_loading fake_root, fake_root + '/lib' do - root = ActiveSupport::Dependencies.load_paths.first + root = ActiveSupport::Dependencies.autoload_paths.first assert_equal ["Lib::A::B", "A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(root + '/lib/a/b') end end @@ -272,7 +272,7 @@ class DependenciesTest < Test::Unit::TestCase def test_loadable_constants_for_path_should_uniq_results fake_root = '/usr/apps/backpack/lib' with_loading fake_root, fake_root + '/' do - root = ActiveSupport::Dependencies.load_paths.first + root = ActiveSupport::Dependencies.autoload_paths.first assert_equal ["A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(root + '/a/b') end end @@ -344,7 +344,7 @@ class DependenciesTest < Test::Unit::TestCase def test_file_search with_loading 'dependencies' do - root = ActiveSupport::Dependencies.load_paths.first + root = ActiveSupport::Dependencies.autoload_paths.first assert_equal nil, ActiveSupport::Dependencies.search_for_file('service_three') assert_equal nil, ActiveSupport::Dependencies.search_for_file('service_three.rb') assert_equal root + '/service_one.rb', ActiveSupport::Dependencies.search_for_file('service_one') @@ -354,14 +354,14 @@ class DependenciesTest < Test::Unit::TestCase def test_file_search_uses_first_in_load_path with_loading 'dependencies', 'autoloading_fixtures' do - deps, autoload = ActiveSupport::Dependencies.load_paths + deps, autoload = ActiveSupport::Dependencies.autoload_paths assert_match %r/dependencies/, deps assert_match %r/autoloading_fixtures/, autoload assert_equal deps + '/conflict.rb', ActiveSupport::Dependencies.search_for_file('conflict') end with_loading 'autoloading_fixtures', 'dependencies' do - autoload, deps = ActiveSupport::Dependencies.load_paths + autoload, deps = ActiveSupport::Dependencies.autoload_paths assert_match %r/dependencies/, deps assert_match %r/autoloading_fixtures/, autoload @@ -418,7 +418,7 @@ class DependenciesTest < Test::Unit::TestCase def test_removal_from_tree_should_be_detected with_loading 'dependencies' do - root = ActiveSupport::Dependencies.load_paths.first + root = ActiveSupport::Dependencies.autoload_paths.first c = ServiceOne ActiveSupport::Dependencies.clear assert ! defined?(ServiceOne) @@ -433,7 +433,7 @@ class DependenciesTest < Test::Unit::TestCase def test_references_should_work with_loading 'dependencies' do - root = ActiveSupport::Dependencies.load_paths.first + root = ActiveSupport::Dependencies.autoload_paths.first c = ActiveSupport::Dependencies.ref("ServiceOne") service_one_first = ServiceOne assert_equal service_one_first, c.get @@ -460,9 +460,9 @@ class DependenciesTest < Test::Unit::TestCase end end - def test_load_once_paths_do_not_add_to_autoloaded_constants + def test_autoload_once_paths_do_not_add_to_autoloaded_constants with_autoloading_fixtures do - ActiveSupport::Dependencies.load_once_paths = ActiveSupport::Dependencies.load_paths.dup + ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_paths.dup assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder") assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") @@ -473,7 +473,7 @@ class DependenciesTest < Test::Unit::TestCase end ensure Object.class_eval { remove_const :ModuleFolder } - ActiveSupport::Dependencies.load_once_paths = [] + ActiveSupport::Dependencies.autoload_once_paths = [] end def test_application_should_special_case_application_controller @@ -760,20 +760,20 @@ class DependenciesTest < Test::Unit::TestCase def test_load_once_constants_should_not_be_unloaded with_autoloading_fixtures do - ActiveSupport::Dependencies.load_once_paths = ActiveSupport::Dependencies.load_paths + ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_paths ::A.to_s assert defined?(A) ActiveSupport::Dependencies.clear assert defined?(A) end ensure - ActiveSupport::Dependencies.load_once_paths = [] + ActiveSupport::Dependencies.autoload_once_paths = [] Object.class_eval { remove_const :A if const_defined?(:A) } end - def test_load_once_paths_should_behave_when_recursively_loading + def test_autoload_once_paths_should_behave_when_recursively_loading with_loading 'dependencies', 'autoloading_fixtures' do - ActiveSupport::Dependencies.load_once_paths = [ActiveSupport::Dependencies.load_paths.last] + ActiveSupport::Dependencies.autoload_once_paths = [ActiveSupport::Dependencies.autoload_paths.last] assert !defined?(CrossSiteDependency) assert_nothing_raised { CrossSiteDepender.nil? } assert defined?(CrossSiteDependency) @@ -784,7 +784,7 @@ class DependenciesTest < Test::Unit::TestCase "CrossSiteDependency shouldn't have been unloaded!" end ensure - ActiveSupport::Dependencies.load_once_paths = [] + ActiveSupport::Dependencies.autoload_once_paths = [] end def test_hook_called_multiple_times diff --git a/activesupport/test/descendants_tracker_test.rb b/activesupport/test/descendants_tracker_test.rb new file mode 100644 index 0000000000..ff24e310de --- /dev/null +++ b/activesupport/test/descendants_tracker_test.rb @@ -0,0 +1,75 @@ +require 'abstract_unit' +require 'test/unit' +require 'active_support' +require 'active_support/core_ext/hash/slice' + +class DescendantsTrackerTest < Test::Unit::TestCase + class Parent + extend ActiveSupport::DescendantsTracker + end + + class Child1 < Parent + end + + class Child2 < Parent + end + + class Grandchild1 < Child1 + end + + class Grandchild2 < Child1 + end + + ALL = [Parent, Child1, Child2, Grandchild1, Grandchild2] + + def test_descendants + assert_equal [Child1, Grandchild1, Grandchild2, Child2], Parent.descendants + assert_equal [Grandchild1, Grandchild2], Child1.descendants + assert_equal [], Child2.descendants + end + + def test_direct_descendants + assert_equal [Child1, Child2], Parent.direct_descendants + assert_equal [Grandchild1, Grandchild2], Child1.direct_descendants + assert_equal [], Child2.direct_descendants + end + + def test_clear_with_autoloaded_parent_children_and_granchildren + mark_as_autoloaded(*ALL) do + ActiveSupport::DescendantsTracker.clear + assert ActiveSupport::DescendantsTracker.descendants.slice(*ALL).empty? + end + end + + def test_clear_with_autoloaded_children_and_granchildren + mark_as_autoloaded Child1, Grandchild1, Grandchild2 do + ActiveSupport::DescendantsTracker.clear + assert_equal [Child2], Parent.descendants + assert_equal [], Child2.descendants + end + end + + def test_clear_with_autoloaded_granchildren + mark_as_autoloaded Grandchild1, Grandchild2 do + ActiveSupport::DescendantsTracker.clear + assert_equal [Child1, Child2], Parent.descendants + assert_equal [], Child1.descendants + assert_equal [], Child2.descendants + end + end + + protected + + def mark_as_autoloaded(*klasses) + old_autoloaded = ActiveSupport::Dependencies.autoloaded_constants.dup + ActiveSupport::Dependencies.autoloaded_constants = klasses.map(&:name) + + old_descendants = ActiveSupport::DescendantsTracker.descendants.dup + old_descendants.each { |k, v| old_descendants[k] = v.dup } + + yield + ensure + ActiveSupport::Dependencies.autoloaded_constants = old_autoloaded + ActiveSupport::DescendantsTracker.descendants.replace(old_descendants) + end +end
\ No newline at end of file diff --git a/activesupport/test/file_update_checker_test.rb b/activesupport/test/file_update_checker_test.rb new file mode 100644 index 0000000000..baf29cc337 --- /dev/null +++ b/activesupport/test/file_update_checker_test.rb @@ -0,0 +1,56 @@ +require 'abstract_unit' +require 'test/unit' +require 'active_support' +require 'fileutils' + +MTIME_FIXTURES_PATH = File.expand_path("../fixtures", __FILE__) + +class FileUpdateCheckerTest < Test::Unit::TestCase + FILES = %w(1.txt 2.txt 3.txt) + + def setup + FileUtils.touch(FILES) + end + + def teardown + FileUtils.rm(FILES) + end + + def test_should_not_execute_the_block_if_no_paths_are_given + i = 0 + checker = ActiveSupport::FileUpdateChecker.new([]){ i += 1 } + checker.execute_if_updated + assert_equal 0, i + end + + def test_should_invoke_the_block_on_first_call_if_it_does_not_calculate_last_updated_at_on_load + i = 0 + checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } + checker.execute_if_updated + assert_equal 1, i + end + + def test_should_not_invoke_the_block_on_first_call_if_it_calculates_last_updated_at_on_load + i = 0 + checker = ActiveSupport::FileUpdateChecker.new(FILES, true){ i += 1 } + checker.execute_if_updated + assert_equal 0, i + end + + def test_should_not_invoke_the_block_if_no_file_has_changed + i = 0 + checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } + 5.times { checker.execute_if_updated } + assert_equal 1, i + end + + def test_should_invoke_the_block_if_a_file_has_changed + i = 0 + checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } + checker.execute_if_updated + sleep(1) + FileUtils.touch(FILES) + checker.execute_if_updated + assert_equal 2, i + end +end diff --git a/railties/test/log_subscriber_test.rb b/activesupport/test/log_subscriber_test.rb index a3a755ae62..0c1f3c51ed 100644 --- a/railties/test/log_subscriber_test.rb +++ b/activesupport/test/log_subscriber_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' -require 'rails/log_subscriber/test_helper' +require 'active_support/log_subscriber/test_helper' -class MyLogSubscriber < Rails::LogSubscriber +class MyLogSubscriber < ActiveSupport::LogSubscriber attr_reader :event def some_event(event) @@ -25,7 +25,7 @@ class MyLogSubscriber < Rails::LogSubscriber end class SyncLogSubscriberTest < ActiveSupport::TestCase - include Rails::LogSubscriber::TestHelper + include ActiveSupport::LogSubscriber::TestHelper def setup super @@ -34,7 +34,7 @@ class SyncLogSubscriberTest < ActiveSupport::TestCase def teardown super - Rails::LogSubscriber.log_subscribers.clear + ActiveSupport::LogSubscriber.log_subscribers.clear end def instrument(*args, &block) @@ -49,7 +49,7 @@ class SyncLogSubscriberTest < ActiveSupport::TestCase end def test_set_color_for_messages - Rails::LogSubscriber.colorize_logging = true + ActiveSupport::LogSubscriber.colorize_logging = true @log_subscriber.bar(nil) assert_equal "\e[31mcool\e[0m, \e[1m\e[34misn't it?\e[0m", @logger.logged(:info).last end @@ -60,56 +60,56 @@ class SyncLogSubscriberTest < ActiveSupport::TestCase end def test_event_is_sent_to_the_registered_class - Rails::LogSubscriber.add :my_log_subscriber, @log_subscriber + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber instrument "some_event.my_log_subscriber" wait assert_equal %w(some_event.my_log_subscriber), @logger.logged(:info) end def test_event_is_an_active_support_notifications_event - Rails::LogSubscriber.add :my_log_subscriber, @log_subscriber + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber instrument "some_event.my_log_subscriber" wait assert_kind_of ActiveSupport::Notifications::Event, @log_subscriber.event end def test_does_not_send_the_event_if_it_doesnt_match_the_class - Rails::LogSubscriber.add :my_log_subscriber, @log_subscriber + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber instrument "unknown_event.my_log_subscriber" wait # If we get here, it means that NoMethodError was not raised. end def test_does_not_send_the_event_if_logger_is_nil - Rails.logger = nil + ActiveSupport::LogSubscriber.logger = nil @log_subscriber.expects(:some_event).never - Rails::LogSubscriber.add :my_log_subscriber, @log_subscriber + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber instrument "some_event.my_log_subscriber" wait end def test_does_not_fail_with_non_namespaced_events - Rails::LogSubscriber.add :my_log_subscriber, @log_subscriber + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber instrument "whatever" wait end def test_flushes_loggers - Rails::LogSubscriber.add :my_log_subscriber, @log_subscriber - Rails::LogSubscriber.flush_all! + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber + ActiveSupport::LogSubscriber.flush_all! assert_equal 1, @logger.flush_count end def test_flushes_the_same_logger_just_once - Rails::LogSubscriber.add :my_log_subscriber, @log_subscriber - Rails::LogSubscriber.add :another, @log_subscriber - Rails::LogSubscriber.flush_all! + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber + ActiveSupport::LogSubscriber.attach_to :another, @log_subscriber + ActiveSupport::LogSubscriber.flush_all! wait assert_equal 1, @logger.flush_count end def test_logging_does_not_die_on_failures - Rails::LogSubscriber.add :my_log_subscriber, @log_subscriber + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber instrument "puke.my_log_subscriber" instrument "some_event.my_log_subscriber" wait diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index f7a5834527..610295fa89 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -49,13 +49,15 @@ class MultibyteCharsTest < Test::Unit::TestCase end def test_should_concatenate - assert_equal 'ab', 'a'.mb_chars + 'b' - assert_equal 'ab', 'a' + 'b'.mb_chars - assert_equal 'ab', 'a'.mb_chars + 'b'.mb_chars + mb_a = 'a'.mb_chars + mb_b = 'b'.mb_chars + assert_equal 'ab', mb_a + 'b' + assert_equal 'ab', 'a' + mb_b + assert_equal 'ab', mb_a + mb_b - assert_equal 'ab', 'a'.mb_chars << 'b' - assert_equal 'ab', 'a' << 'b'.mb_chars - assert_equal 'ab', 'a'.mb_chars << 'b'.mb_chars + assert_equal 'ab', mb_a << 'b' + assert_equal 'ab', 'a' << mb_b + assert_equal 'abb', mb_a << mb_b end def test_consumes_utf8_strings @@ -395,6 +397,7 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase assert_equal 'こ', @chars.slice(0) assert_equal 'わ', @chars.slice(3) assert_equal nil, ''.mb_chars.slice(-1..1) + assert_equal nil, ''.mb_chars.slice(-1, 1) assert_equal '', ''.mb_chars.slice(0..10) assert_equal 'にちわ', @chars.slice(1..3) assert_equal 'にちわ', @chars.slice(1, 3) @@ -443,6 +446,11 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase assert_equal 'Abc', 'abc'.mb_chars.capitalize end + def test_titleize_should_work_on_ascii_characters + assert_equal '', ''.mb_chars.titleize + assert_equal 'Abc Abc', 'abc abc'.mb_chars.titleize + end + def test_respond_to_knows_which_methods_the_proxy_responds_to assert ''.mb_chars.respond_to?(:slice) # Defined on Chars assert ''.mb_chars.respond_to?(:capitalize!) # Defined on Chars @@ -480,6 +488,15 @@ class MultibyteCharsExtrasTest < Test::Unit::TestCase end end + def test_titleize_should_be_unicode_aware + assert_equal "Él Que Se Enteró", chars("ÉL QUE SE ENTERÓ").titleize + assert_equal "Абвг Абвг", chars("аБвг аБвг").titleize + end + + def test_titleize_should_not_affect_characters_that_do_not_case_fold + assert_equal "日本語", chars("日本語").titleize + end + def test_limit_should_not_break_on_blank_strings example = chars('') assert_equal example, example.limit(0) diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb index 0f36f5204d..3d1bae163f 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/json' class OrderedHashTest < Test::Unit::TestCase def setup @@ -185,6 +186,12 @@ class OrderedHashTest < Test::Unit::TestCase assert @ordered_hash.inspect.include?(@hash.inspect) end + def test_json + ordered_hash = ActiveSupport::OrderedHash[:foo, :bar] + hash = Hash[:foo, :bar] + assert_equal ordered_hash.to_json, hash.to_json + end + def test_alternate_initialization_with_splat alternate = ActiveSupport::OrderedHash[1,2,3,4] assert_kind_of ActiveSupport::OrderedHash, alternate @@ -244,4 +251,16 @@ class OrderedHashTest < Test::Unit::TestCase assert_equal @ordered_hash.keys, @deserialized_ordered_hash.keys assert_equal @ordered_hash.values, @deserialized_ordered_hash.values end + + def test_update_sets_keys + @updated_ordered_hash = ActiveSupport::OrderedHash.new + @updated_ordered_hash.update(:name => "Bob") + assert_equal [:name], @updated_ordered_hash.keys + end + + def test_invert + expected = ActiveSupport::OrderedHash[@values.zip(@keys)] + assert_equal expected, @ordered_hash.invert + assert_equal @values.zip(@keys), @ordered_hash.invert.to_a + end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 620623b389..af6eee69e5 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -268,7 +268,7 @@ class TimeZoneTest < Test::Unit::TestCase end def test_index - assert_not_nil ActiveSupport::TimeZone["bogus"] + assert_nil ActiveSupport::TimeZone["bogus"] assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone["Central Time (US & Canada)"] assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone[8] assert_raise(ArgumentError) { ActiveSupport::TimeZone[false] } @@ -285,6 +285,11 @@ class TimeZoneTest < Test::Unit::TestCase assert_equal(-21_600, zone.utc_offset) end + def test_unknown_zones_dont_store_mapping_keys + ActiveSupport::TimeZone["bogus"] + assert !ActiveSupport::TimeZone.zones_map.key?("bogus") + end + def test_new assert_equal ActiveSupport::TimeZone["Central Time (US & Canada)"], ActiveSupport::TimeZone.new("Central Time (US & Canada)") end @@ -1,3 +1,5 @@ +#!/usr/bin/env ruby + begin require "rails/cli" rescue LoadError diff --git a/railties/CHANGELOG b/railties/CHANGELOG index c33a4d11dd..8e2bac7f00 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,6 +1,13 @@ *Rails 3.0.0 [Release Candidate] (unreleased)* +* Rails no longer autoload code in lib for application. You need to explicitly require it. [José Valim] + +* Rails::LogSubscriber was renamed to ActiveSupport::LogSubscriber [José Valim] + +* config.load_(once_)paths in config/application.rb got renamed to config.autoload_(once_)paths. [fxn] + * Abort generation/booting on Ruby 1.9.1. [fxn] + * Made the rails command work even when you're in a subdirectory [Chad Fowler] diff --git a/railties/Rakefile b/railties/Rakefile index bf19961b59..ddc872e18b 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -18,11 +18,11 @@ task :test => 'test:isolated' namespace :test do task :isolated do dir = ENV["TEST_DIR"] || "**" - Dir["test/#{dir}/*_test.rb"].all? do |file| + Dir["test/#{dir}/*_test.rb"].each do |file| next true if file.include?("fixtures") ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) system(ruby, '-Itest', "-I#{File.dirname(__FILE__)}/../activesupport/lib", file) - end or raise "Failures" + end end end diff --git a/railties/guides/assets/javascripts/code_highlighter.js b/railties/guides/assets/javascripts/code_highlighter.js index ce983dad52..ce983dad52 100755..100644 --- a/railties/guides/assets/javascripts/code_highlighter.js +++ b/railties/guides/assets/javascripts/code_highlighter.js diff --git a/railties/guides/assets/javascripts/guides.js b/railties/guides/assets/javascripts/guides.js index c4e4d459ea..c4e4d459ea 100755..100644 --- a/railties/guides/assets/javascripts/guides.js +++ b/railties/guides/assets/javascripts/guides.js diff --git a/railties/guides/assets/stylesheets/print.css b/railties/guides/assets/stylesheets/print.css index 628da105d4..628da105d4 100755..100644 --- a/railties/guides/assets/stylesheets/print.css +++ b/railties/guides/assets/stylesheets/print.css diff --git a/railties/guides/assets/stylesheets/reset.css b/railties/guides/assets/stylesheets/reset.css index cb14fbcc55..cb14fbcc55 100755..100644 --- a/railties/guides/assets/stylesheets/reset.css +++ b/railties/guides/assets/stylesheets/reset.css diff --git a/railties/guides/assets/stylesheets/style.css b/railties/guides/assets/stylesheets/style.css index 89b2ab885a..89b2ab885a 100755..100644 --- a/railties/guides/assets/stylesheets/style.css +++ b/railties/guides/assets/stylesheets/style.css diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile index 9d8426b5de..038ca903c1 100644 --- a/railties/guides/source/action_controller_overview.textile +++ b/railties/guides/source/action_controller_overview.textile @@ -338,6 +338,25 @@ end Note that while for session values you set the key to +nil+, to delete a cookie value you should use +cookies.delete(:key)+. +h3. Rendering xml and json data + +ActionController makes it extremely easy to render +xml+ or +json+ data. If you generate a controller using scaffold then your controller would look something like this. + +<ruby> +class UsersController < ApplicationController + def index + @users = User.all + respond_to do |format| + format.html # index.html.erb + format.xml { render :xml => @users} + end + end +end +</ruby> + +Notice that in the above case code is <tt>render :xml => @users</tt> and not <tt>render :xml => @users.to_xml</tt>. That is because if the input is not string then rails automatically invokes +to_xml+ . + + h3. Filters Filters are methods that are run before, after or "around" a controller action. diff --git a/railties/guides/source/active_record_basics.textile b/railties/guides/source/active_record_basics.textile index d81e461e63..e6ef2cdd20 100644 --- a/railties/guides/source/active_record_basics.textile +++ b/railties/guides/source/active_record_basics.textile @@ -32,16 +32,16 @@ Active Record gives us several mechanisms, the most important being the ability * Validate models before they get persisted to the database * Perform database operations in an object-oriented fashion. -h3. Convention over Configuration in ActiveRecord +h3. Convention over Configuration in Active Record -When writing applications using other programming languages or frameworks, it may be necessary to write a lot of configuration code. This is particularly true for ORM frameworks in general. However, if you follow the conventions adopted by Rails, you'll need to write very little configuration (in some case no configuration at all) when creating ActiveRecord models. The idea is that if you configure your applications in the very same way most of the times then this should be the default way. In this cases, explicit configuration would be needed only in those cases where you can't follow the conventions for any reason. +When writing applications using other programming languages or frameworks, it may be necessary to write a lot of configuration code. This is particularly true for ORM frameworks in general. However, if you follow the conventions adopted by Rails, you'll need to write very little configuration (in some case no configuration at all) when creating Active Record models. The idea is that if you configure your applications in the very same way most of the times then this should be the default way. In this cases, explicit configuration would be needed only in those cases where you can't follow the conventions for any reason. h4. Naming Conventions -By default, ActiveRecord uses some naming conventions to find out how the mapping between models and database tables should be created. Rails will pluralize your class names to find the respective database table. So, for a class +Book+, you should have a database table called *books*. The Rails pluralization mechanisms are very powerful, being capable to pluralize (and singularize) both regular and irregular words. When using class names composed of two or more words, the model class name should follow the Ruby conventions, using the camelCase form, while the table name must contain the words separated by underscores. Examples: +By default, Active Record uses some naming conventions to find out how the mapping between models and database tables should be created. Rails will pluralize your class names to find the respective database table. So, for a class +Book+, you should have a database table called *books*. The Rails pluralization mechanisms are very powerful, being capable to pluralize (and singularize) both regular and irregular words. When using class names composed of two or more words, the model class name should follow the Ruby conventions, using the camelCase form, while the table name must contain the words separated by underscores. Examples: -* Database Table - Plural with underscores separating words i.e. (book_clubs) -* Model Class - Singular with the first letter of each word capitalized i.e. (BookClub) +* Database Table - Plural with underscores separating words (e.g., book_clubs) +* Model Class - Singular with the first letter of each word capitalized (e.g., BookClub) |_.Model / Class |_.Table / Schema | |Post |posts| @@ -53,30 +53,32 @@ By default, ActiveRecord uses some naming conventions to find out how the mappin h4. Schema Conventions -ActiveRecord uses naming conventions for the columns in database tables, depending on the purpose of these columns. +Active Record uses naming conventions for the columns in database tables, depending on the purpose of these columns. -* *Foreign keys* - These fields should be named following the pattern table_id i.e. (item_id, order_id). These are the fields that ActiveRecord will look for when you create associations between your models. -* *Primary keys* - By default, ActiveRecord will use an integer column named "id" as the table's primary key. When using "Rails Migrations":migrations.html to create your tables, this column will be automatically created. +* *Foreign keys* - These fields should be named following the pattern table_id (e.g., item_id, order_id). These are the fields that Active Record will look for when you create associations between your models. +* *Primary keys* - By default, Active Record will use an integer column named "id" as the table's primary key. When using "Rails Migrations":migrations.html to create your tables, this column will be automatically created. -There are also some optional column names that will create additional features to ActiveRecord instances: +There are also some optional column names that will create additional features to Active Record instances: -* *created_at / created_on* - ActiveRecord will store the current date and time to this field when creating the record. -* *updated_at / updated_on* - ActiveRecord will store the current date and times to this field when updating the record. +* *created_at* - Automatically gets set to the current date and time when the record is first created. +* *created_on* - Automatically gets set to the current date when the record is first created. +* *updated_at* - Automatically gets set to the current date and time whenever the record is updated. +* *updated_on* - Automatically gets set to the current date whenever the record is updated. * *lock_version* - Adds "optimistic locking":http://api.rubyonrails.com/classes/ActiveRecord/Locking.html to a model. * *type* - Specifies that the model uses "Single Table Inheritance":http://api.rubyonrails.com/classes/ActiveRecord/Base.html * *(table_name)_count* - Used to cache the number of belonging objects on associations. For example, a +comments_count+ column in a +Post+ class that has many instances of +Comment+ will cache the number of existent comments for each post. -NOTE: While these column names are optional they are in fact reserved by ActiveRecord. Steer clear of reserved keywords unless you want the extra functionality. For example, "type" is a reserved keyword used to designate a table using Single Table Inheritance. If you are not using STI, try an analogous keyword like "context", that may still accurately describe the data you are modeling. +NOTE: While these column names are optional they are in fact reserved by Active Record. Steer clear of reserved keywords unless you want the extra functionality. For example, "type" is a reserved keyword used to designate a table using Single Table Inheritance. If you are not using STI, try an analogous keyword like "context", that may still accurately describe the data you are modeling. -h3. Creating ActiveRecord Models +h3. Creating Active Record Models -It's very easy to create ActiveRecord models. All you have to do is to subclass the ActiveRecord::Base class and you're good to go: +It's very easy to create Active Record models. All you have to do is to subclass the +ActiveRecord::Base+ class and you're good to go: <ruby> class Product < ActiveRecord::Base; end </ruby> -This will create a +Product+ model, mapped to a *products* table at the database. By doing this you'll also have the ability to map the columns of each row in that table with the attributes of the instances of your model. So, suppose that the *products* table was created using a SQL sentence like: +This will create a +Product+ model, mapped to a *products* table at the database. By doing this you'll also have the ability to map the columns of each row in that table with the attributes of the instances of your model. So, suppose that the *products* table was created using an SQL sentence like: <sql> CREATE TABLE products ( @@ -99,12 +101,15 @@ h3. Overriding the Naming Conventions What if you need to follow a different naming convention or need to use your Rails application with a legacy database? No problem, you can easily override the default conventions. You can use the +ActiveRecord::Base.set_table_name+ method to specify the table name that should be used: + <ruby> class Product < ActiveRecord::Base set_table_name "PRODUCT" end </ruby> + If you do so, you will have to define manually the class name that is hosting the fixtures (class_name.yml) using the +set_fixture_class+ method in your test definition: + <ruby> class FunnyJoke < ActiveSupport::TestCase set_fixture_class :funny_jokes => 'Joke' @@ -113,7 +118,8 @@ class FunnyJoke < ActiveSupport::TestCase end </ruby> -It's also possible to override the column that should be used as the table's primary key. Use the +ActiveRecord::Base.set_primary_key+ method for that: +It's also possible to override the column that should be used as the table's primary key using the +ActiveRecord::Base.set_primary_key+ method: + <ruby> class Product < ActiveRecord::Base set_primary_key "product_id" @@ -122,7 +128,7 @@ end h3. Reading and Writing Data -CRUD is an acronym for the four verbs we use to operate on data: Create, Read, Update, Delete. Active Record automatically creates methods to allow an application to read and manipulate data stored within its tables. +CRUD is an acronym for the four verbs we use to operate on data: *C*reate, *R*ead, *U*pdate and *D*elete. Active Record automatically creates methods to allow an application to read and manipulate data stored within its tables. h4. Create @@ -155,7 +161,7 @@ Finally, passing a block to either create or new will return a new User object: h4. Read -ActiveRecord provides a rich API for accessing data within a database. Below are a few examples of different data access methods provided by ActiveRecord. +Active Record provides a rich API for accessing data within a database. Below are a few examples of different data access methods provided by Active Record. <ruby> # return all records @@ -163,7 +169,7 @@ ActiveRecord provides a rich API for accessing data within a database. Below are </ruby> <ruby> - # return first record + # return the first record user = User.first </ruby> @@ -201,11 +207,11 @@ Likewise, once retrieved an Active Record object can be destroyed which removes h3. Validations -Active Record allows you to validate the state of a model before it gets written into the database. There are several methods that you can use to check your models and validate that an attribute value is not empty, is unique and not already in the database, follows a specific format and many more. You can learn more about validations in the "Active Record Validations and Callbacks guide":activerecord_validations_callbacks.html#validations-overview. +Active Record allows you to validate the state of a model before it gets written into the database. There are several methods that you can use to check your models and validate that an attribute value is not empty, is unique and not already in the database, follows a specific format and many more. You can learn more about validations in the "Active Record Validations and Callbacks guide":active_record_validations_callbacks.html#validations-overview. h3. Callbacks -Active Record callbacks allow you to attach code to certain events in the life-cycle of your models. This enables you to add behavior to your models by transparently executing code when those events occur, like when you create a new record, update it, destroy it and so on. You can learn more about callbacks in the "Active Record Validations and Callbacks guide":activerecord_validations_callbacks.html#callbacks-overview. +Active Record callbacks allow you to attach code to certain events in the life-cycle of your models. This enables you to add behavior to your models by transparently executing code when those events occur, like when you create a new record, update it, destroy it and so on. You can learn more about callbacks in the "Active Record Validations and Callbacks guide":active_record_validations_callbacks.html#callbacks-overview. h3. Migrations diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile index 73f3ebafbe..f5e70aef41 100644 --- a/railties/guides/source/active_record_querying.textile +++ b/railties/guides/source/active_record_querying.textile @@ -51,7 +51,7 @@ Active Record will perform queries on the database for you and is compatible wit h3. Retrieving Objects from the Database -To retrieve objects from the database, Active Record provides several finder methods. These methods allows you to pass arguments into it to perform certain queries on your database without the need of writing raw SQL. +To retrieve objects from the database, Active Record provides several finder methods. Each finder method allows you to pass arguments into it to perform certain queries on your database without writing raw SQL. The methods are: * +where+ @@ -66,7 +66,7 @@ The methods are: * +readonly+ * +from+ -All of these methods return a Relation +All of the above methods return an instance of <tt>ActiveRecord::Relation</tt>. Primary operation of <tt>Model.find(options)</tt> can be summarized as: diff --git a/railties/guides/source/activerecord_validations_callbacks.textile b/railties/guides/source/active_record_validations_callbacks.textile index 1f9bc1279a..cfd4ae55cc 100644 --- a/railties/guides/source/activerecord_validations_callbacks.textile +++ b/railties/guides/source/active_record_validations_callbacks.textile @@ -16,7 +16,7 @@ endprologue. h3. The Object Lifecycle -During the normal operation of a Rails application objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object lifecycle</em> so that you can control your application and its data. +During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object lifecycle</em> so that you can control your application and its data. Validations allow you to ensure that only valid data is stored in your database. Callbacks and observers allow you to trigger logic before or after an alteration of an object's state. @@ -71,7 +71,7 @@ The following methods trigger validations, and will save the object to the datab * +update_attributes+ * +update_attributes!+ -The bang versions (e.g. +save!+) raise an exception if the record is invalid. The non-bang versions don't: +save+ and +update_attributes+ return +false+, +create+ and +update+ just return the object/s. +The bang versions (e.g. +save!+) raise an exception if the record is invalid. The non-bang versions don't: +save+ and +update_attributes+ return +false+, +create+ and +update+ just return the objects. h4. Skipping Validations @@ -240,9 +240,9 @@ class Account < ActiveRecord::Base end </ruby> -The +validates_exclusion_of+ helper has an option +:in+ that receives the set of values that will not be accepted for the validated attributes. The +:in+ option has an alias called +:within+ that you can use for the same purpose, if you'd like to. This example uses the +:message+ option to show how you can include the attribute's value. +The +validates_exclusion_of+ helper has an option +:in+ that receives the set of values that will not be accepted for the validated attributes. The +:in+ option has an alias called +:within+ that you can use for the same purpose, if you'd like to. This example uses the +:message+ option to show how you can include the attribute's value. -The default error message for +validates_exclusion_of+ is "_is reserved_". +The default error message for +validates_exclusion_of+ is "_is reserved_". h4. +validates_format_of+ @@ -270,7 +270,7 @@ end The +validates_inclusion_of+ helper has an option +:in+ that receives the set of values that will be accepted. The +:in+ option has an alias called +:within+ that you can use for the same purpose, if you'd like to. The previous example uses the +:message+ option to show how you can include the attribute's value. -The default error message for +validates_inclusion_of+ is "_is not included in the list_". +The default error message for +validates_inclusion_of+ is "_is not included in the list_". h4. +validates_length_of+ @@ -343,7 +343,7 @@ Besides +:only_integer+, the +validates_numericality_of+ helper also accepts the * +:greater_than_or_equal_to+ - Specifies the value must be greater than or equal to the supplied value. The default error message for this option is "_must be greater than or equal to %{count}_". * +:equal_to+ - Specifies the value must be equal to the supplied value. The default error message for this option is "_must be equal to %{count}_". * +:less_than+ - Specifies the value must be less than the supplied value. The default error message for this option is "_must be less than %{count}_". -* +:less_than_or_equal_to+ - Specifies the value must be less than or equal the supplied value. The default error message for this option is "_must be less or equal to %{count}_". +* +:less_than_or_equal_to+ - Specifies the value must be less than or equal the supplied value. The default error message for this option is "_must be less than or equal to %{count}_". * +:odd+ - Specifies the value must be an odd number if set to true. The default error message for this option is "_must be odd_". * +:even+ - Specifies the value must be an even number if set to true. The default error message for this option is "_must be even_". @@ -374,7 +374,7 @@ The default error message for +validates_presence_of+ is "_can't be empty_". h4. +validates_uniqueness_of+ -This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, you must create an unique index in your database. +This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, you must create a unique index in your database. <ruby> class Account < ActiveRecord::Base @@ -423,14 +423,14 @@ class GoodnessValidator < ActiveRecord::Validator end </ruby> -The +validates_with+ helper takes a class, or a list of classes to use for validation. There is no default error message for +validates_with+. You must manually add errors to the record's errors collection in the validator class. +The +validates_with+ helper takes a class, or a list of classes to use for validation. There is no default error message for +validates_with+. You must manually add errors to the record's errors collection in the validator class. The validator class has two attributes by default: * +record+ - the record to be validated * +options+ - the extra options that were passed to +validates_with+ -Like all other validations, +validates_with+ takes the +:if+, +:unless+ and +:on+ options. If you pass any other options, it will send those options to the validator class as +options+: +Like all other validations, +validates_with+ takes the +:if+, +:unless+ and +:on+ options. If you pass any other options, it will send those options to the validator class as +options+: <ruby> class Person < ActiveRecord::Base @@ -494,7 +494,7 @@ As you've already seen, the +:message+ option lets you specify the message that h4. +:on+ -The +:on+ option lets you specify when the validation should happen. The default behavior for all the built-in validation helpers is to be ran on save (both when you're creating a new record and when you're updating it). If you want to change it, you can use +:on => :create+ to run the validation only when a new record is created or +:on => :update+ to run the validation only when a record is updated. +The +:on+ option lets you specify when the validation should happen. The default behavior for all the built-in validation helpers is to be run on save (both when you're creating a new record and when you're updating it). If you want to change it, you can use +:on => :create+ to run the validation only when a new record is created or +:on => :update+ to run the validation only when a record is updated. <ruby> class Person < ActiveRecord::Base @@ -573,7 +573,7 @@ class Invoice < ActiveRecord::Base end </ruby> -You can even create your own validation helpers and reuse them in several different models. For example, an application that manages surveys may find useful to express that a certain field corresponds to a set of choices: +You can even create your own validation helpers and reuse them in several different models. For example, an application that manages surveys may find it useful to express that a certain field corresponds to a set of choices: <ruby> ActiveRecord::Base.class_eval do @@ -599,7 +599,7 @@ The following is a list of the most commonly used methods. Please refer to the + h4(#working_with_validation_errors-errors). +errors+ -Returns an OrderedHash with all errors. Each key is the attribute name and value is an array of strings with all errors. +Returns an OrderedHash with all errors. Each key is the attribute name and the value is an array of strings with all errors. <ruby> class Person < ActiveRecord::Base @@ -619,7 +619,7 @@ person.errors # => [] h4(#working_with_validation_errors-errors-2). +errors[]+ -+errors[]+ is used when you want to check the error messages for a specific attribute. It returns an array of strings with all error messages for the given attribute, each string with one error message. If there are no errors related to the attribute returns an empty array. ++errors[]+ is used when you want to check the error messages for a specific attribute. It returns an array of strings with all error messages for the given attribute, each string with one error message. If there are no errors related to the attribute, it returns an empty array. <ruby> class Person < ActiveRecord::Base @@ -681,7 +681,7 @@ Another way to do this is using +[]=+ setter h4. +errors[:base]+ -You can add errors messages that are related to the object's state as a whole, instead of being related to a specific attribute. You can use this method when you want to say that the object is invalid, no matter the values of its attributes. Since +errors[:base]+ is an array, you can simply add a string to the array and uses it as the error message. +You can add error messages that are related to the object's state as a whole, instead of being related to a specific attribute. You can use this method when you want to say that the object is invalid, no matter the values of its attributes. Since +errors[:base]+ is an array, you can simply add a string to the array and uses it as the error message. <ruby> class Person < ActiveRecord::Base @@ -789,7 +789,7 @@ Both the +form.error_messages+ and the +error_messages_for+ helpers accept optio :header_tag => :h3 %> </erb> -Which results in the following content +Which results in the following content: !images/customized_error_messages.png(Customized error messages)! diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index 1c17609b0a..844b9428bd 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -24,7 +24,7 @@ h5. Cherry-picking a Definition The most lightweight way to get +blank?+ is to cherry-pick the file that defines it. -For every single method defined as a core extension this guide has a note that says where is such a method defined. In the case of +blank?+ the note reads: +For every single method defined as a core extension this guide has a note that says where such a method is defined. In the case of +blank?+ the note reads: NOTE: Defined in +active_support/core_ext/object/blank.rb+. @@ -124,7 +124,7 @@ NOTE: Defined in +active_support/core_ext/object/blank.rb+. h4. +duplicable?+ -A few fundamental objects in Ruby are singletons. For example, in the whole live of a program the integer 1 refers always to the same instance: +A few fundamental objects in Ruby are singletons. For example, in the whole life of a program the integer 1 refers always to the same instance: <ruby> 1.object_id # => 3 @@ -159,7 +159,7 @@ NOTE: Defined in +active_support/core_ext/object/duplicable.rb+. h4. +returning+ -The method +returning+ yields its argument to a block and returns it. You tipically use it with a mutable object that gets modified in the block: +The method +returning+ yields its argument to a block and returns it. You typically use it with a mutable object that gets modified in the block: <ruby> def html_options_for_form(url_for_options, options, *parameters_for_url) @@ -187,7 +187,7 @@ def log_info(sql, name, ms) end </ruby> -You can shorten that using +Object#try+. This method is a synonim for +Object#send+ except that it returns +nil+ if sent to +nil+. The previous example could then be rewritten as: +You can shorten that using +Object#try+. This method is a synonym for +Object#send+ except that it returns +nil+ if sent to +nil+. The previous example could then be rewritten as: <ruby> def log_info(sql, name, ms) @@ -209,8 +209,7 @@ String.singleton_class # => #<Class:String> String.new.singleton_class # => #<Class:#<String:0x17a1d1c>> </ruby> -WARNING: Fixnums and symbols have no singleton classes, +singleton_class+ -raises +TypeError+ on them. Moreover, the singleton classes of +nil+, +true+, and +false+, are +NilClass+, +TrueClass+, and +FalseClass+, respectively. +WARNING: Fixnums and symbols have no singleton classes, +singleton_class+ raises +TypeError+ on them. Moreover, the singleton classes of +nil+, +true+, and +false+, are +NilClass+, +TrueClass+, and +FalseClass+, respectively. NOTE: Defined in +active_support/core_ext/kernel/singleton_class.rb+. @@ -256,7 +255,7 @@ NOTE: Defined in +active_support/core_ext/object/acts_like.rb+. h4. +to_param+ -All objects in Rails respond to the method +to_param+, which is meant to return something that represents them as values in a query string, or as a URL fragments. +All objects in Rails respond to the method +to_param+, which is meant to return something that represents them as values in a query string, or as URL fragments. By default +to_param+ just calls +to_s+: @@ -294,7 +293,7 @@ we get: user_path(@user) # => "/users/357-john-smith" </ruby> -WARNING. Controllers need to be aware of any redifinition of +to_param+ because when a request like that comes in "357-john-smith" is the value of +params[:id]+. +WARNING. Controllers need to be aware of any redefinition of +to_param+ because when a request like that comes in "357-john-smith" is the value of +params[:id]+. NOTE: Defined in +active_support/core_ext/object/to_param.rb+. @@ -332,7 +331,7 @@ Arrays return the result of applying +to_query+ to each element with <tt>_key_[] # => "sample%5B%5D=3.4&sample%5B%5D=-45.6" </ruby> -Hashes also respond to +to_query+ but with a different signature. If no argument is passed a call generates a sorted series of key/value assigments calling +to_query(key)+ on its values. Then it joins the result with "&": +Hashes also respond to +to_query+ but with a different signature. If no argument is passed a call generates a sorted series of key/value assignments calling +to_query(key)+ on its values. Then it joins the result with "&": <ruby> {:c => 3, :b => 2, :a => 1}.to_query # => "a=1&b=2&c=3" @@ -656,9 +655,9 @@ h5. Internal Attributes When you are defining an attribute in a class that is meant to be subclassed name collisions are a risk. That's remarkably important for libraries. -Active Support defines the macros +attr_internal_reader+, +attr_internal_writer+, and +attr_internal_accessor+. They behave like their Ruby builtin +attr_*+ counterparts, except they name the unerlying instace variable in a way that makes collisions less likely. +Active Support defines the macros +attr_internal_reader+, +attr_internal_writer+, and +attr_internal_accessor+. They behave like their Ruby builtin +attr_*+ counterparts, except they name the underlying instance variable in a way that makes collisions less likely. -The macro +attr_internal+ is a synonim for +attr_internal_accessor+: +The macro +attr_internal+ is a synonym for +attr_internal_accessor+: <ruby> # library @@ -721,7 +720,7 @@ h4. Method Delegation The class method +delegate+ offers an easy way to forward methods. -For example, if +User+ has some details like the age factored out to +Profile+, it could be handy to still be able to acces such attribute directly, <tt>user.age</tt>, instead of having to explicit the chain <tt>user.profile.age</tt>. +For example, if +User+ has some details like the age factored out to +Profile+, it could be handy to still be able to access such attributes directly, <tt>user.age</tt>, instead of having to explicit the chain <tt>user.profile.age</tt>. That can be accomplished by hand: @@ -935,7 +934,7 @@ NOTE: Defined in +active_support/core_ext/module/synchronization.rb+. h4. Reachable -A named module is reachable if it is stored in its correspoding constant. It means you can reach the module object via the constant. +A named module is reachable if it is stored in its corresponding constant. It means you can reach the module object via the constant. That is what ordinarily happens, if a module is called "M", the +M+ constant exists and holds it: @@ -1016,7 +1015,7 @@ h3. Extensions to +Class+ h4. Class Attributes -The method +Class#class_attribute+ declares one or more inheritable class attributes that can be overriden at any level down the hierarchy: +The method +Class#class_attribute+ declares one or more inheritable class attributes that can be overridden at any level down the hierarchy: <ruby> class A @@ -1088,7 +1087,7 @@ NOTE: Defined in +active_support/core_ext/class/attribute_accessors.rb+. h4. Class Inheritable Attributes -Class variables are shared down the inheritance tree. Class instance variables are not shared, but they are not inherited either. The macros +class_inheritable_reader+, +class_inheritable_writer+, and +class_inheritable_accessor+ provide accesors for class-level data which is inherited but not shared with children: +Class variables are shared down the inheritance tree. Class instance variables are not shared, but they are not inherited either. The macros +class_inheritable_reader+, +class_inheritable_writer+, and +class_inheritable_accessor+ provide accessors for class-level data which is inherited but not shared with children: <ruby> module ActionController @@ -1204,7 +1203,7 @@ s.html_safe? # => true s # => "<script>...</script>" </ruby> -It is your responsability to ensure calling +html_safe+ on a particular string is fine. +It is your responsibility to ensure calling +html_safe+ on a particular string is fine. NOTE: For performance reasons safe strings are implemented in a way that cannot offer an in-place +html_safe!+ variant. @@ -1377,7 +1376,7 @@ The method +pluralize+ returns the plural of its receiver: "equipment".pluralize # => "equipment" </ruby> -As the previous example shows, Active Support knows some irregular plurals and uncountable nouns. Builtin rules can be extended in +config/initializers/inflections.rb+. That file is generated by the +rails+ command and has instructions in comments. +As the previous example shows, Active Support knows some irregular plurals and uncountable nouns. Built-in rules can be extended in +config/initializers/inflections.rb+. That file is generated by the +rails+ command and has instructions in comments. Active Record uses this method to compute the default table name that corresponds to a model: @@ -1697,6 +1696,90 @@ foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key NOTE: Defined in +active_support/core_ext/string/inflections.rb+. +h4. Conversions + +h5. +constantize+ + +The method +constantize+ expects the receiver to contain the name of a constant, and tries to get you the object stored in there, assuming it is defined: + +<ruby> +"ActiveRecord::Base".constantize # => ActiveRecord::Base +</ruby> + +The name is assumed to be top-level, no matter whether it starts with "::" or not. No lexical context is taken into account: + +<ruby> +C = 1 +module M + C = 2 + "C".constantize # => 1, same as "::C".constantize +end +</ruby> + +NOTE: Defined in +active_support/core_ext/string/conversions.rb+. + +h5. +ord+ + +Ruby 1.9 defines +ord+ to be the codepoint of the first character of the receiver. Active Support backports +ord+ for single-byte encondings like ASCII or ISO-8859-1 in Ruby 1.8: + +<ruby> +"a".ord # => 97 +"à".ord # => 224, in ISO-8859-1 +</ruby> + +In Ruby 1.8 +ord+ doesn't work in general in UTF8 strings, use the multibyte support in Active Support for that: + +<ruby> +"a".mb_chars.ord # => 97 +"à".mb_chars.ord # => 224, in UTF8 +</ruby> + +Note that the 224 is different in both examples. In ISO-8859-1 "à" is represented as a single byte, 224. Its single-character representattion in UTF8 has two bytes, namely 195 and 160, but its Unicode codepoint is 224. If we call +ord+ on the UTF8 string "à" the return value will be 195 in Ruby 1.8. That is not an error, because UTF8 is unsupported, the call itself would be bogus. + +INFO: +ord+ is equivalent to +getbyte(0)+. + +NOTE: Defined in +active_support/core_ext/string/conversions.rb+. + +h5. +getbyte+ + +Active Support backports +getbyte+ from Ruby 1.9: + +<ruby> +"foo".getbyte(0) # => 102, same as "foo".ord +"foo".getbyte(1) # => 111 +"foo".getbyte(9) # => nil +"foo".getbyte(-1) # => 111 +</ruby> + +INFO: +getbyte+ is equivalent to +[]+. + +NOTE: Defined in +active_support/core_ext/string/conversions.rb+. + +h5. +to_date+, +to_time+, +to_datetime+ + +The methods +to_date+, +to_time+, and +to_datetime+ are basically convenience wrappers around +Date._parse+: + +<ruby> +"2010-07-27".to_date # => Tue, 27 Jul 2010 +"2010-07-27 23:37:00".to_time # => Tue Jul 27 23:37:00 UTC 2010 +"2010-07-27 23:37:00".to_datetime # => Tue, 27 Jul 2010 23:37:00 +0000 +</ruby> + ++to_time+ receives an optional argument +:utc+ or +:local+, to indicate which time zone you want the time in: + +<ruby> +"2010-07-27 23:42:00".to_time(:utc) # => Tue Jul 27 23:42:00 UTC 2010 +"2010-07-27 23:42:00".to_time(:local) # => Tue Jul 27 23:42:00 +0200 2010 +</ruby> + +Default is +:utc+. + +Please refer to the documentation of +Date._parse+ for further details. + +INFO: The three of them return +nil+ for blank receivers. + +NOTE: Defined in +active_support/core_ext/string/conversions.rb+. + h3. Extensions to +Numeric+ h4. Bytes @@ -1760,7 +1843,7 @@ h3. Extensions to +Float+ h4. +round+ -The builtin method +Float#round+ rounds a float to the nearest integer. Active Support adds an optional parameter to let you specify a precision: +The built-in method +Float#round+ rounds a float to the nearest integer. Active Support adds an optional parameter to let you specify a precision: <ruby> Math::E.round(4) # => 2.7183 @@ -1925,7 +2008,7 @@ Similarly, +from+ returns the tail from the element at the passed index on: [].from(0) # => [] </ruby> -The methods +second+, +third+, +fourth+, and +fifth+ return the corresponding element (+first+ is builtin). Thanks to social wisdom and positive constructiveness all around, +forty_two+ is also available. +The methods +second+, +third+, +fourth+, and +fifth+ return the corresponding element (+first+ is built-in). Thanks to social wisdom and positive constructiveness all around, +forty_two+ is also available. NOTE: Defined in +active_support/core_ext/array/access.rb+. @@ -2120,7 +2203,7 @@ NOTE: Defined in +active_support/core_ext/array/conversions.rb+. h4. Wrapping -The class method +Array.wrap+ behaves like the function +Array()+ except that it does not try to call +to_a+ on its argument. That changes the behaviour for enumerables: +The class method +Array.wrap+ behaves like the function +Array()+ except that it does not try to call +to_a+ on its argument. That changes the behavior for enumerables: <ruby> Array.wrap(:foo => :bar) # => [{:foo => :bar}] @@ -2282,7 +2365,7 @@ NOTE: Defined in +active_support/core_ext/hash/conversions.rb+. h4. Merging -Ruby has a builtin method +Hash#merge+ that merges two hashes: +Ruby has a built-in method +Hash#merge+ that merges two hashes: <ruby> {:a => 1, :b => 1}.merge(:a => 0, :c => 2) @@ -2511,7 +2594,7 @@ NOTE: Defined in +active_support/core_ext/hash/keys.rb+. h4. Slicing -Ruby has builtin support for taking slices out of strings and arrays. Active Support extends slicing to hashes: +Ruby has built-in support for taking slices out of strings and arrays. Active Support extends slicing to hashes: <ruby> {:a => 1, :b => 2, :c => 3}.slice(:a, :c) @@ -2625,7 +2708,7 @@ Active Support extends this method so that the argument may be another range in (1...9).include?(3..9) # => false </ruby> -WARNING: The orginal +Range#include?+ is still the one aliased to +Range#===+. +WARNING: The original +Range#include?+ is still the one aliased to +Range#===+. NOTE: Defined in +active_support/core_ext/range/include_range.rb+. @@ -3080,7 +3163,7 @@ Active Support adds +missing_name?+ to +NameError+, which tests whether the exce The name may be given as a symbol or string. A symbol is tested against the bare constant name, a string is against the fully-qualified constant name. -TIP: A symbol can represent a fully-qualified constant name as in +:"ActiveRecord::Base"+, so the behaviour for symbols is defined for convenience, not because it has to be that way technically. +TIP: A symbol can represent a fully-qualified constant name as in +:"ActiveRecord::Base"+, so the behavior for symbols is defined for convenience, not because it has to be that way technically. For example, when an action of +PostsController+ is called Rails tries optimistically to use +PostsHelper+. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that +posts_helper.rb+ raises a +NameError+ due to an actual unknown constant. That should be reraised. The method +missing_name?+ provides a way to distinguish both cases: diff --git a/railties/guides/source/form_helpers.textile b/railties/guides/source/form_helpers.textile index 515b3aad39..1f1b7d076e 100644 --- a/railties/guides/source/form_helpers.textile +++ b/railties/guides/source/form_helpers.textile @@ -205,7 +205,7 @@ Upon form submission the value entered by the user will be stored in +params[:pe WARNING: You must pass the name of an instance variable, i.e. +:person+ or +"person"+, not an actual instance of your model object. -Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the "Active Record Validations and Callbacks":./activerecord_validations_callbacks.html#displaying-validation-errors-in-the-view guide. +Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the "Active Record Validations and Callbacks":./active_record_validations_callbacks.html#displaying-validation-errors-in-the-view guide. h4. Binding a Form to an Object diff --git a/railties/guides/source/generators.textile b/railties/guides/source/generators.textile index 704a8793b2..c5b41673e1 100644 --- a/railties/guides/source/generators.textile +++ b/railties/guides/source/generators.textile @@ -1,21 +1,21 @@ -h2. Creating and customizing Rails Generators +h2. Creating and Customizing Rails Generators Rails generators are an essential tool if you plan to improve your workflow and in this guide you will learn how to create and customize already existing generators. In this guide you will: -* Learn how to see which generators are available in your application; -* Create a generator using templates; -* Learn how Rails searches for generators before invoking them; -* Customize your scaffold by creating new generators; -* Customize your scaffold by changing generators templates; -* Learn how to use fallbacks to avoid overwriting a huge set of generators; +* Learn how to see which generators are available in your application +* Create a generator using templates +* Learn how Rails searches for generators before invoking them +* Customize your scaffold by creating new generators +* Customize your scaffold by changing generator templates +* Learn how to use fallbacks to avoid overwriting a huge set of generators endprologue. NOTE: This guide is about Rails generators for versions >= 3.0. Rails generators from previous versions are not supported. -h3. First contact +h3. First Contact When you create an application using the +rails+ command, you are in fact using a Rails generator. After that, you can get a list of all available generators by just invoking +rails generate+: @@ -25,15 +25,15 @@ $ cd myapp $ rails generate </shell> -You will get a list of all generators that comes with Rails. If you need a detailed description, for instance about the helper generator, you can simply do: +You will get a list of all generators that comes with Rails. If you need a detailed description of the helper generator, for example, you can simply do: <shell> $ rails generate helper --help </shell> -h3. Creating your first generator +h3. Creating Your First Generator -Since Rails 3.0, generators are built on top of "Thor":http://github.com/wycats/thor. Thor has a powerful options parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named +initializer.rb+ inside +config/initializers+. +Since Rails 3.0, generators are built on top of "Thor":http://github.com/wycats/thor. Thor provides powerful options parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named +initializer.rb+ inside +config/initializers+. The first step is to create a file at +RAILS_APP/lib/generators/initializer_generator.rb+ with the following content: @@ -45,7 +45,7 @@ class InitializerGenerator < Rails::Generators::Base end </ruby> -Our new generator is quite simple: it inherits from +Rails::Generators::Base+ and have one method definition. Each public method in the generator is executed when a generator is invoked. Finally, we invoke the +create_file+ method that will create a file at the given destination with the given content. If you are familiar with Rails Application Templates API, you are at home with new generators API. +Our new generator is quite simple: it inherits from +Rails::Generators::Base+ and has one method definition. Each public method in the generator is executed when a generator is invoked. Finally, we invoke the +create_file+ method that will create a file at the given destination with the given content. If you are familiar with the Rails Application Templates API, you'll feel right at home with the new generators API. To invoke our new generator, we just need to do: @@ -59,7 +59,7 @@ Before we go on, let's see our brand new generator description: $ rails generate initializer --help </shell> -Rails usually is able to generate good descriptions if a generator is namespaced, as +ActiveRecord::Generators::ModelGenerator+, but not in this particular case. We can solve this problem in two ways. The first one is calling +desc+ inside our generator: +Rails is usually able to generate good descriptions if a generator is namespaced, as +ActiveRecord::Generators::ModelGenerator+, but not in this particular case. We can solve this problem in two ways. The first one is calling +desc+ inside our generator: <ruby> class InitializerGenerator < Rails::Generators::Base @@ -70,9 +70,9 @@ class InitializerGenerator < Rails::Generators::Base end </ruby> -Now we can see the new description by invoking +--help+ in the new generator. The second way to add a description is by creating a file named +USAGE+ in the same directory as our generator. We are going to do that in the next step. +Now we can see the new description by invoking +--help+ on the new generator. The second way to add a description is by creating a file named +USAGE+ in the same directory as our generator. We are going to do that in the next step. -h3. Creating generators with generators +h3. Creating Generators with Generators A faster way to create a generator is using the generator's generator: @@ -84,7 +84,7 @@ $ rails generate generator initializer create lib/generators/initializer/templates </shell> -And it will create a new generator as follow: +And it will create a new generator as follows: <ruby> class InitializerGenerator < Rails::Generators::NamedBase @@ -92,7 +92,7 @@ class InitializerGenerator < Rails::Generators::NamedBase end </ruby> -At first, we can notice that we are inheriting from +Rails::Generators::NamedBase+ instead of +Rails::Generators::Base+. This means that our generator expects as least one argument, which will be the name of the initializer. +First, notice that we are inheriting from +Rails::Generators::NamedBase+ instead of +Rails::Generators::Base+. This means that our generator expects as least one argument, which will be the name of the initializer. We can see that by invoking the description of this new generator (don't forget to delete the old generator file): @@ -127,13 +127,13 @@ And let's execute our generator: $ rails generate initializer foo </shell> -We can see that now a initializer named foo was created at +config/initializers/foo.rb+ with the contents of our template. That means that copy_file copied a file in our source root to the destination path we gave. The method +file_name+ is automatically created when we inherit from +Rails::Generators::NamedBase+. +We can see that now a initializer named foo was created at +config/initializers/foo.rb+ with the contents of our template. That means that +copy_file+ copied a file in our source root to the destination path we gave. The method +file_name+ is automatically created when we inherit from +Rails::Generators::NamedBase+. -h3. Generators lookup +h3. Generators Lookup -With our first generator created, we must discuss briefly generators lookup. The way Rails finds generators is exactly the same way Ruby find files, i.e. using +$LOAD_PATHS+. +Now that we've created our first generator, we need to briefly discuss generator lookup. The way Rails finds generators is exactly the same way Ruby find files, i.e. using +$LOAD_PATHS+. -For instance, when you say +rails g initializer foo+, rails knows you want to invoke the initializer generator and then search for the following generators in the $LOAD_PATHS: +For instance, when you say +rails generate initializer foo+, Rails knows you want to invoke the initializer generator and then search for the following generators in the $LOAD_PATHS: <shell> rails/generators/initializer/initializer_generator.rb @@ -144,7 +144,7 @@ generators/initializer_generator.rb If none of them is found, it raises an error message. -h3. Customizing your workflow +h3. Customizing Your Workflow Rails generators are flexible enough to let you customize your scaffold the way you want. In your +config/application.rb+ there is a section just for generators: @@ -156,7 +156,7 @@ config.generators do |g| end </ruby> -Before we customize our workflow, let's first see how our scaffold looks like: +Before we customize our workflow, let's first see what our scaffold looks like: <shell> $ rails generate scaffold User name:string @@ -186,7 +186,7 @@ $ rails generate scaffold User name:string create public/stylesheets/scaffold.css </shell> -Looking at this output, is easy to understand how generators work on Rails 3.0 and above. The scaffold generator actually doesn't generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. +Looking at this output, it's easy to understand how generators work on Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. Our first customization on the workflow will be to stop generating stylesheets and test fixtures on scaffold. We can achieve that by changing our application to the following: @@ -199,15 +199,15 @@ config.generators do |g| end </ruby> -If we generate another resource on scaffold, we can notice that neither stylesheets nor fixtures are created anymore. If you want to customize it further, for example to use +Datamapper+ and +Rspec+ instead of +ActiveRecord+ and +TestUnit+, is just a matter of adding their gems to your application and configuring your generators. +If we generate another resource on scaffold, we can notice that neither stylesheets nor fixtures are created anymore. If you want to customize it further, for example to use +Datamapper+ and +RSpec+ instead of +ActiveRecord+ and +TestUnit+, it's just a matter of adding their gems to your application and configuring your generators. -To show that, we are going to create a new helper generator that simply adds some instance variable readers. First, we create a generator: +To demonstrate this, we are going to create a new helper generator that simply adds some instance variable readers. First, we create a generator: <shell> $ rails generate generator my_helper </shell> -After that, we can delete both templates directory and the +source_root+ class method from our new generators, because we are not going to need them. So our new generator looks like the following: +After that, we can delete both the +templates+ directory and the +source_root+ class method from our new generators, because we are not going to need them. So our new generator looks like the following: <ruby> class MyHelperGenerator < Rails::Generators::NamedBase @@ -227,7 +227,7 @@ We can try out our new generator by creating a helper for users: $ rails generate my_helper users </shell> -And it will generate the following helper file in app/helpers: +And it will generate the following helper file in +app/helpers+: <ruby> module UsersHelper @@ -258,7 +258,7 @@ $ rails generate scaffold Post body:text We can notice on the output that our new helper was invoked instead of the Rails default. However one thing is missing, which is tests for our new generator and to do that, we are going to reuse old helpers test generators. -Since Rails 3.0, this is easy to do due to the hooks concept. Our new helper does not need to be focused in one specific test framework, it can simply provide a hook and a test framework just need to implement this hook in order to be compatible. +Since Rails 3.0, this is easy to do due to the hooks concept. Our new helper does not need to be focused in one specific test framework, it can simply provide a hook and a test framework just needs to implement this hook in order to be compatible. To do that, we can change your generator to the following: @@ -276,7 +276,7 @@ end end </ruby> -Now, when the helper generator is invoked and let's say test unit is configured as test framework, it will try to invoke both +MyHelper::Generators::TestUnitGenerator+ and +TestUnit::Generators::MyHelperGenerator+. Since none of those are defined, we can tell our generator to invoke +TestUnit::Generators::HelperGenerator+ instead, which is defined since it's a Rails generator. To do that, we just need to add: +Now, when the helper generator is invoked and TestUnit is configured as the test framework, it will try to invoke both +MyHelper::Generators::TestUnitGenerator+ and +TestUnit::Generators::MyHelperGenerator+. Since none of those are defined, we can tell our generator to invoke +TestUnit::Generators::HelperGenerator+ instead, which is defined since it's a Rails generator. To do that, we just need to add: <ruby> # Search for :helper instead of :my_helper @@ -285,11 +285,11 @@ Now, when the helper generator is invoked and let's say test unit is configured And now you can re-run scaffold for another resource and see it generating tests as well! -h3. Customizing your workflow by changing generators templates +h3. Customizing Your Workflow by Changing Generators Templates In the step above, we simply wanted to add a line to the generated helper, without adding any extra functionality. There is a simpler way to do that, and it's by replacing the templates of already existing generators. -In Rails 3.0 and above, generators does not look only in the source root for templates, they also search for templates in other paths. And one of them is inside +RAILS_APP/lib/templates+. Since we want to customize +Rails::Generators::HelperGenerator+, we can do that by simple making a template copy inside +RAILS_APP/lib/templates/rails/helper+ with the name +helper.rb+. So let's create such file with the following content: +In Rails 3.0 and above, generators don't just look in the source root for templates, they also search for templates in other paths. And one of them is inside +RAILS_APP/lib/templates+. Since we want to customize +Rails::Generators::HelperGenerator+, we can do that by simply making a template copy inside +RAILS_APP/lib/templates/rails/helper+ with the name +helper.rb+. So let's create that file with the following content: <erb> module <%= class_name %>Helper @@ -310,9 +310,9 @@ end If you generate another resource, you can see that we got exactly the same result! This is useful if you want to customize your scaffold templates and/or layout by just creating +edit.html.erb+, +index.html.erb+ and so on inside +RAILS_APP/lib/templates/erb/scaffold+. -h3. Adding generators fallbacks +h3. Adding Generators Fallbacks -One last feature about generators which is quite useful for plugin generators is fallbacks. For example, imagine that you want to add a feature on top of TestUnit test framework, like "shoulda":http://github.com/thoughtbot/shoulda does. Since TestUnit already implements all generators required by Rails and shoulda just want to overwrite part of it, there is no need for shoulda to reimplement some generators again, they can simply tell Rails to use a +TestUnit+ generator if none was found under +Shoulda+ namespace. +One last feature about generators which is quite useful for plugin generators is fallbacks. For example, imagine that you want to add a feature on top of TestUnit test framework, like "shoulda":http://github.com/thoughtbot/shoulda does. Since TestUnit already implements all generators required by Rails and shoulda just wants to overwrite part of it, there is no need for shoulda to reimplement some generators again, it can simply tell Rails to use a +TestUnit+ generator if none was found under the +Shoulda+ namespace. We can easily simulate this behavior by changing our +config/application.rb+ once again: @@ -324,11 +324,11 @@ config.generators do |g| g.stylesheets false # Add a fallback! - g.fallbacks[:should] = :test_unit + g.fallbacks[:shoulda] = :test_unit end </ruby> -Now, if create a Comment scaffold, you will see that shoulda generators are being invoked, and at the end, they are just falling back to test unit generators: +Now, if you create a Comment scaffold, you will see that the shoulda generators are being invoked, and at the end, they are just falling back to test unit generators: <shell> $ rails generate scaffold Comment body:text @@ -357,7 +357,7 @@ $ rails generate scaffold Comment body:text create test/unit/helpers/comments_helper_test.rb </shell> -Such tool allows your generators to have single responsibility, increasing the code reuse and reducing the amount of duplication. +Fallbacks allow your generators to have a single responsibility, increasing code reuse and reducing the amount of duplication. h3. Changelog diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile index 7b4f1ca471..6abb3ed9f4 100644 --- a/railties/guides/source/getting_started.textile +++ b/railties/guides/source/getting_started.textile @@ -227,7 +227,7 @@ development: timeout: 5000 </yaml> -NOTE: In this guide we are using an SQLite3 database for data storage, this is because it is a zero configuration database that just works. Rails also supports MySQL and PostgreSQL "out of the box", and has plugins for many database systems, if you are using a database in a production environment, Rails most likely has an adapter for it. +NOTE: In this guide we are using an SQLite3 database for data storage, because it is a zero configuration database that just works. Rails also supports MySQL and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it. h5. Configuring a MySQL Database diff --git a/railties/guides/source/index.html.erb b/railties/guides/source/index.html.erb index 5a715cf9f7..be077fcd2f 100644 --- a/railties/guides/source/index.html.erb +++ b/railties/guides/source/index.html.erb @@ -47,7 +47,7 @@ Ruby on Rails Guides <p>This guide covers how you can use Active Record migrations to alter your database in a structured and organized manner.</p> <% end %> -<%= guide("Active Record Validations and Callbacks", 'activerecord_validations_callbacks.html') do %> +<%= guide("Active Record Validations and Callbacks", 'active_record_validations_callbacks.html') do %> <p>This guide covers how you can use Active Record validations and callbacks.</p> <% end %> diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index bc3a2d0f0b..e458413b35 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -148,7 +148,7 @@ Here the only two gems we need are +rails+ and +sqlite3-ruby+, so it seems. This * mime-types-1.16.gem * polyglot-0.3.1.gem * rack-1.1.0.gem -* rack-mount-0.6.4.gem +* rack-mount-0.6.5.gem * rack-test-0.5.4.gem * rails-3.0.0.beta4.gem * railties-3.0.0.beta4.gem diff --git a/railties/guides/source/layout.html.erb b/railties/guides/source/layout.html.erb index f1727166ba..501d8fef6d 100644 --- a/railties/guides/source/layout.html.erb +++ b/railties/guides/source/layout.html.erb @@ -50,7 +50,7 @@ <dd><a href="getting_started.html">Getting Started with Rails</a></dd> <dt>Models</dt> <dd><a href="migrations.html">Rails Database Migrations</a></dd> - <dd><a href="activerecord_validations_callbacks.html">Active Record Validations and Callbacks</a></dd> + <dd><a href="active_record_validations_callbacks.html">Active Record Validations and Callbacks</a></dd> <dd><a href="association_basics.html">Active Record Associations</a></dd> <dd><a href="active_record_querying.html">Active Record Query Interface</a></dd> <dt>Views</dt> diff --git a/railties/guides/source/rails_application_templates.textile b/railties/guides/source/rails_application_templates.textile index 1af6f56957..1bf9cfec33 100644 --- a/railties/guides/source/rails_application_templates.textile +++ b/railties/guides/source/rails_application_templates.textile @@ -51,11 +51,11 @@ h4. gem(name, options = {}) Adds a +config.gem+ entry for the supplied gem to the generated application’s +config/environment.rb+. -For example, if your application depends on the gems +bj+ and +hpricot+ : +For example, if your application depends on the gems +bj+ and +nokogiri+ : <ruby> gem "bj" -gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net" +gem "nokogiri" </ruby> Please note that this will NOT install the gems for you. So you may want to run the +rake gems:install+ task too : diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 54af42a03f..9ff06856c3 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -32,7 +32,7 @@ the request is dispatched to the +patients+ controller's +show+ action with <tt> h4. Generating URLs from Code -You can also generate routes. If your application contains this code: +You can also generate URLs. If your application contains this code: <ruby> @patient = Patient.find(17) @@ -308,7 +308,7 @@ You are not limited to the seven routes that RESTful routing creates by default. h5. Adding Member Routes -To add a member route, just add +member+ block into resource block: +To add a member route, just add a +member+ block into the resource block: <ruby> resources :photos do @@ -318,9 +318,9 @@ resources :photos do end </ruby> -This will recognize +/photos/1/preview+ with GET, and route to the +preview+ action of +PhotosController+. It will also create the +preview_photo_url+ and +preview_photo_path+ helpers. +This will recognize +/photos/1/preview+ with GET, and route to the +preview+ action of +PhotosController+. It will also create the +preview_photo_url+ and +preview_photo_path+ helpers. -Within the block of member routes, each route name specifies the HTTP verb that it will recognize. You can use +get+, +put+, +post+, or +delete+ here. If you don't have multiple +member+ routes, you can also passing +:on+ to a route. +Within the block of member routes, each route name specifies the HTTP verb that it will recognize. You can use +get+, +put+, +post+, or +delete+ here. If you don't have multiple +member+ routes, you can also pass +:on+ to a route, eliminating the block: <ruby> resources :photos do @@ -340,9 +340,9 @@ resources :photos do end </ruby> -This will enable Rails to recognize URLs such as +/photos/search+ with GET, and route to the +search+ action of +PhotosController+. It will also create the +search_photos_url+ and +search_photos_path+ route helpers. +This will enable Rails to recognize URLs such as +/photos/search+ with GET, and route to the +search+ action of +PhotosController+. It will also create the +search_photos_url+ and +search_photos_path+ route helpers. -Just as with member routes, you can pass +:on+ to a route. +Just as with member routes, you can pass +:on+ to a route: <ruby> resources :photos do @@ -384,7 +384,7 @@ An incoming URL of +/photos/show/1/2+ will be dispatched to the +show+ action of h4. Static Segments -You can specify static segments when creating a route. +You can specify static segments when creating a route: <ruby> match ':controller/:action/:id/with_user/:user_id' @@ -444,6 +444,21 @@ This route would match URLs such as +/photo/A12345+. You can more succinctly exp match 'photo/:id' => 'photos#show', :id => /[A-Z]\d{5}/ </ruby> ++:constraints+ takes regular expression. However note that regexp anchors can't be used within constraints. For example following route will not work: + +<ruby> +match '/:id' => 'posts#show', :constraints => {:id => /^\d/} +</ruby> + +However, note that you don't need to use anchors because all routes are anchored at the start. + +For example, the following routes would allow for +posts+ with +to_param+ values like +1-hello-world+ that always begin with a number and +users+ with +to_param+ values like +david+ that never begin with a number to share the root namespace: + +<ruby> +match '/:id' => 'posts#show', :constraints => { :id => /\d.+/ } +match '/:username' => 'users#show' +</ruby> + h4. Request-Based Constraints You can also constrain a route based on any method on the <a href="action_controller_overview.html#the-request-object">Request</a> object that returns a +String+. @@ -575,7 +590,7 @@ resources :photos, :constraints => {:id => /[A-Z][A-Z][0-9]+/} This declaration constrains the +:id+ parameter to match the supplied regular expression. So, in this case, the router would no longer match +/photos/1+ to this route. Instead, +/photos/RR27+ would match. -You can specify a single constraint to a apply to a number of routes by using the block form: +You can specify a single constraint to apply to a number of routes by using the block form: <ruby> constraints(:id => /[A-Z][A-Z][0-9]+/) do diff --git a/railties/guides/w3c_validator.rb b/railties/guides/w3c_validator.rb index b55645a7a9..4da48bf3fb 100644 --- a/railties/guides/w3c_validator.rb +++ b/railties/guides/w3c_validator.rb @@ -35,23 +35,27 @@ module RailsGuides def validate validator = MarkupValidator.new + STDOUT.sync = true + errors_on_guides = {} guides_to_validate.each do |f| - puts "Validating #{f}" results = validator.validate_file(f) - if !results.validity - puts "#{f} FAILED W3C validation with #{results.errors.size} error(s):" - results.errors.each do |error| - puts error.to_s - end + if results.validity + print "." + else + print "E" + errors_on_guides[f] = results.errors end end + + show_results(errors_on_guides) end private def guides_to_validate guides = Dir["./guides/output/*.html"] + guides.delete("./guides/output/layout.html") ENV.key?('ONLY') ? select_only(guides) : guides end @@ -61,6 +65,26 @@ module RailsGuides prefixes.any? {|p| guide.start_with?("./guides/output/#{p}")} end end + + def show_results(error_list) + if error_list.size == 0 + puts "\n\nAll checked guides validate OK!" + else + error_summary = error_detail = "" + + error_list.each_pair do |name, errors| + error_summary += "\n #{name}" + error_detail += "\n\n #{name} has #{errors.size} validation error(s):\n" + errors.each do |error| + error_detail += "\n "+error.to_s.gsub("\n", "") + end + end + + puts "\n\nThere are #{error_list.size} guides with validation errors:\n" + error_summary + puts "\nHere are the detailed errors for each guide:" + error_detail + end + end + end end diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index ed66f493e6..bbf28a8c08 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -9,7 +9,6 @@ require 'active_support/core_ext/logger' require 'rails/application' require 'rails/version' require 'rails/deprecation' -require 'rails/log_subscriber' require 'active_support/railtie' require 'action_dispatch/railtie' diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 85ae8cbbb1..4a7ed2d028 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/hash/reverse_merge' +require 'active_support/file_update_checker' require 'fileutils' require 'rails/plugin' require 'rails/engine' @@ -27,7 +28,7 @@ module Rails # Besides providing the same configuration as Rails::Engine and Rails::Railtie, # the application object has several specific configurations, for example # "allow_concurrency", "cache_classes", "consider_all_requests_local", "filter_parameters", - # "logger", "reload_engines", "reload_plugins" and so forth. + # "logger", "reload_plugins" and so forth. # # Check Rails::Application::Configuration to see them all. # @@ -46,7 +47,6 @@ module Rails autoload :Configuration, 'rails/application/configuration' autoload :Finisher, 'rails/application/finisher' autoload :Railties, 'rails/application/railties' - autoload :RoutesReloader, 'rails/application/routes_reloader' class << self private :new @@ -67,7 +67,7 @@ module Rails raise "You cannot have more than one Rails::Application" if Rails.application super Rails.application = base.instance - Rails.application.add_lib_to_load_paths! + Rails.application.add_lib_to_load_path! ActiveSupport.run_load_hooks(:before_configuration, base.instance) end @@ -84,17 +84,30 @@ module Rails delegate :middleware, :to => :config - def add_lib_to_load_paths! + # This method is called just after an application inherits from Rails::Application, + # allowing the developer to load classes in lib and use them during application + # configuration. + # + # class MyApplication < Rails::Application + # require "my_backend" # in lib/my_backend + # config.i18n.backend = MyBackend + # end + # + # Notice this method takes into consideration the default root path. So if you + # are changing config.root inside your application definition or having a custom + # Rails application, you will need to add lib to $LOAD_PATH on your own in case + # you need to load files in lib/ during the application configuration as well. + def add_lib_to_load_path! #:nodoc: path = config.root.join('lib').to_s $LOAD_PATH.unshift(path) if File.exists?(path) end - def require_environment! + def require_environment! #:nodoc: environment = paths.config.environment.to_a.first require environment if environment end - def eager_load! + def eager_load! #:nodoc: railties.all(&:eager_load!) super end @@ -108,11 +121,18 @@ module Rails end def routes_reloader - @routes_reloader ||= RoutesReloader.new + @routes_reloader ||= ActiveSupport::FileUpdateChecker.new([]){ reload_routes! } end def reload_routes! - routes_reloader.reload! + routes = Rails::Application.routes + routes.disable_clear_and_finalize = true + + routes.clear! + routes_reloader.paths.each { |path| load(path) } + ActiveSupport.on_load(:action_controller) { routes.finalize! } + ensure + routes.disable_clear_and_finalize = false end def initialize! @@ -140,6 +160,7 @@ module Rails config.middleware.build(routes) end end + alias :build_middleware_stack :app def call(env) app.call(env.reverse_merge!(env_defaults)) @@ -196,10 +217,5 @@ module Rails def initialize_generators require "rails/generators" end - - # Application is always reloadable when config.cache_classes is false. - def reloadable?(app) - true - end end end diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 0a435f0f36..44e26b5713 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -1,4 +1,5 @@ require "active_support/notifications" +require "active_support/descendants_tracker" module Rails class Application @@ -55,6 +56,7 @@ module Rails initializer :set_clear_dependencies_hook do unless config.cache_classes ActionDispatch::Callbacks.after do + ActiveSupport::DescendantsTracker.clear ActiveSupport::Dependencies.clear end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 25e54e9dce..465851c0e6 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -10,7 +10,7 @@ module Rails attr_accessor :allow_concurrency, :cache_classes, :cache_store, :encoding, :consider_all_requests_local, :dependency_loading, :filter_parameters, :log_level, :logger, :middleware, - :plugins, :preload_frameworks, :reload_engines, :reload_plugins, + :plugins, :preload_frameworks, :reload_plugins, :secret_token, :serve_static_assets, :session_options, :time_zone, :whiny_nils @@ -59,7 +59,7 @@ module Rails if File.exists?("#{root}/test/mocks/#{Rails.env}") ActiveSupport::Deprecation.warn "\"Rails.root/test/mocks/#{Rails.env}\" won't be added " << "automatically to load paths anymore in future releases" - paths.mocks_path "test/mocks", :load_path => true, :glob => Rails.env + paths.mocks_path "test/mocks", :autoload => true, :glob => Rails.env end paths @@ -110,7 +110,7 @@ module Rails def colorize_logging=(val) @colorize_logging = val - Rails::LogSubscriber.colorize_logging = val + ActiveSupport::LogSubscriber.colorize_logging = val self.generators.colorize_logging = val end diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index d7ff489336..855467227b 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -7,14 +7,14 @@ module Rails config.generators.templates.unshift(*paths.lib.templates.to_a) end - initializer :ensure_load_once_paths_as_subset do - extra = ActiveSupport::Dependencies.load_once_paths - - ActiveSupport::Dependencies.load_paths + initializer :ensure_autoload_once_paths_as_subset do + extra = ActiveSupport::Dependencies.autoload_once_paths - + ActiveSupport::Dependencies.autoload_paths unless extra.empty? abort <<-end_error - load_once_paths must be a subset of the load_paths. - Extra items in load_once_paths: #{extra * ','} + autoload_once_paths must be a subset of the autoload_paths. + Extra items in autoload_once_paths: #{extra * ','} end_error end end @@ -32,7 +32,7 @@ module Rails end initializer :build_middleware_stack do - app + build_middleware_stack end initializer :eager_load! do diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb deleted file mode 100644 index a2b3622df8..0000000000 --- a/railties/lib/rails/application/routes_reloader.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Rails - class Application - class RoutesReloader - attr_reader :paths - - def initialize - @paths, @last_change_at = [], nil - end - - def changed_at - routes_changed_at = nil - - paths.each do |path| - config_changed_at = File.stat(path).mtime - - if routes_changed_at.nil? || config_changed_at > routes_changed_at - routes_changed_at = config_changed_at - end - end - - routes_changed_at - end - - def reload! - routes = Rails::Application.routes - routes.disable_clear_and_finalize = true - - routes.clear! - paths.each { |path| load(path) } - ActiveSupport.on_load(:action_controller) { routes.finalize! } - - nil - ensure - routes.disable_clear_and_finalize = false - end - - def reload_if_changed - current_change_at = changed_at - if @last_change_at != current_change_at - @last_change_at = current_change_at - reload! - end - end - end - end -end
\ No newline at end of file diff --git a/railties/lib/rails/commands/generate.rb b/railties/lib/rails/commands/generate.rb index 1b3eef504a..1b3eef504a 100755..100644 --- a/railties/lib/rails/commands/generate.rb +++ b/railties/lib/rails/commands/generate.rb diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index 278548558e..b97ff086b6 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -44,6 +44,7 @@ begin $stderr.puts "Run '#{$0} -h' for help." exit 1 elsif File.exist?(code_or_file) + $0 = code_or_file eval(File.read(code_or_file), nil, code_or_file) else eval(code_or_file) diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 3f74cd49fc..9d9dd48ea9 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -9,7 +9,7 @@ module Rails args, options = args.dup, {} opt_parser = OptionParser.new do |opts| - opts.banner = "Usage: rails server [options]" + opts.banner = "Usage: rails server [mongrel, thin, etc] [options]" opts.on("-p", "--port=port", Integer, "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } opts.on("-b", "--binding=ip", String, @@ -83,7 +83,8 @@ module Rails :environment => (ENV['RAILS_ENV'] || "development").dup, :daemonize => false, :debugger => false, - :pid => "tmp/pids/server.pid" + :pid => File.expand_path("tmp/pids/server.pid"), + :config => File.expand_path("config.ru") }) end end diff --git a/railties/lib/rails/console/app.rb b/railties/lib/rails/console/app.rb index 4a7701081b..9d9763699d 100644 --- a/railties/lib/rails/console/app.rb +++ b/railties/lib/rails/console/app.rb @@ -30,5 +30,3 @@ def reload!(print=true) ActionDispatch::Callbacks.new(Proc.new {}, false).call({}) true end - -reload!(false) diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index cdb00a4eff..ee3e3ba040 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -32,14 +32,14 @@ module Rails # == Configuration # # Besides the Railtie configuration which is shared across the application, in a - # Rails::Engine you can access load_paths, eager_load_paths and load_once_paths, + # Rails::Engine you can access autoload_paths, eager_load_paths and autoload_once_paths, # which differently from a Railtie, are scoped to the current Engine. # # Example: # # class MyEngine < Rails::Engine # # Add a load path for this specific Engine - # config.load_paths << File.expand_path("../lib/some/path", __FILE__) + # config.autoload_paths << File.expand_path("../lib/some/path", __FILE__) # # initializer "my_engine.add_middleware" do |app| # app.middleware.use MyEngine::Middleware @@ -142,7 +142,7 @@ module Rails # Add configured load paths to ruby load paths and remove duplicates. initializer :set_load_path, :before => :bootstrap_hook do - config.load_paths.reverse_each do |path| + _all_load_paths.reverse_each do |path| $LOAD_PATH.unshift(path) if File.directory?(path) end $LOAD_PATH.uniq! @@ -154,17 +154,13 @@ module Rails # This needs to be an initializer, since it needs to run once # per engine and get the engine as a block parameter initializer :set_autoload_paths, :before => :bootstrap_hook do |app| - ActiveSupport::Dependencies.load_paths.unshift(*config.load_paths) - - if reloadable?(app) - ActiveSupport::Dependencies.load_once_paths.unshift(*config.load_once_paths) - else - ActiveSupport::Dependencies.load_once_paths.unshift(*config.load_paths) - end + ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths) + ActiveSupport::Dependencies.autoload_once_paths.unshift(*config.autoload_once_paths) # Freeze so future modifications will fail rather than do nothing mysteriously - config.load_paths.freeze - config.load_once_paths.freeze + config.autoload_paths.freeze + config.eager_load_paths.freeze + config.autoload_once_paths.freeze end initializer :add_routing_paths do |app| @@ -195,7 +191,6 @@ module Rails ActiveSupport.on_load(:action_controller) do prepend_view_path(views) end - ActiveSupport.on_load(:action_mailer) do prepend_view_path(views) end @@ -214,8 +209,12 @@ module Rails protected - def reloadable?(app) - app.config.reload_engines + def _all_autoload_paths + @_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq + end + + def _all_load_paths + @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq end end end diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 446fe0bda9..2f465670cf 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -4,7 +4,7 @@ module Rails class Engine class Configuration < ::Rails::Railtie::Configuration attr_reader :root - attr_writer :eager_load_paths, :load_once_paths, :load_paths + attr_writer :eager_load_paths, :autoload_once_paths, :autoload_paths def initialize(root=nil) super() @@ -41,12 +41,12 @@ module Rails @eager_load_paths ||= paths.eager_load end - def load_once_paths - @load_once_paths ||= paths.load_once + def autoload_once_paths + @autoload_once_paths ||= paths.autoload_once end - def load_paths - @load_paths ||= paths.load_paths + def autoload_paths + @autoload_paths ||= paths.autoload_paths end end end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 41aecea355..8794392a7d 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -50,7 +50,6 @@ module Rails :performance_tool => nil, :resource_controller => :controller, :scaffold_controller => :scaffold_controller, - :singleton => false, :stylesheets => true, :test_framework => nil, :template_engine => :erb @@ -334,4 +333,4 @@ module Rails paths end end -end
\ No newline at end of file +end diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 7af329bbf0..199afbdc30 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -275,7 +275,7 @@ module Rails # def route(routing_code) log :route, routing_code - sentinel = "routes.draw do |map|" + sentinel = /\.routes\.draw do(\s*\|map\|)?\s*$/ in_root do inject_into_file 'config/routes.rb', "\n #{routing_code}\n", { :after => sentinel, :verbose => false } diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index bd2260fc29..67a9a6030d 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -3,7 +3,7 @@ begin rescue LoadError puts "Thor is not available.\nIf you ran this command from a git checkout " \ "of Rails, please make sure thor is installed,\nand run this command " \ - "as `ruby /path/to/rails myapp --dev`" + "as `ruby /path/to/rails new myapp --dev`" exit end diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb index 2db7f7bbf3..b2c8d7051b 100644 --- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb @@ -8,17 +8,12 @@ module Erb argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" - class_option :singleton, :type => :boolean, :desc => "Supply to skip index view" - def create_root_folder empty_directory File.join("app/views", controller_file_path) end def copy_view_files - views = available_views - views.delete("index") if options[:singleton] - - views.each do |view| + available_views.each do |view| filename = filename_with_extensions(view) template filename, File.join("app/views", controller_file_path, filename) end diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb index 9b83207b3f..d12b2ff0e5 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -1,10 +1,10 @@ -<%%= form_for(@<%= singular_name %>) do |f| %> - <%% if @<%= singular_name %>.errors.any? %> +<%%= form_for(@<%= singular_table_name %>) do |f| %> + <%% if @<%= singular_table_name %>.errors.any? %> <div id="error_explanation"> - <h2><%%= pluralize(@<%= singular_name %>.errors.count, "error") %> prohibited this <%= singular_name %> from being saved:</h2> + <h2><%%= pluralize(@<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2> <ul> - <%% @<%= singular_name %>.errors.full_messages.each do |msg| %> + <%% @<%= singular_table_name %>.errors.full_messages.each do |msg| %> <li><%%= msg %></li> <%% end %> </ul> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb index 5bc507ffc8..e58b9fbd08 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb @@ -1,6 +1,6 @@ -<h1>Editing <%= singular_name %></h1> +<h1>Editing <%= singular_table_name %></h1> <%%= render 'form' %> -<%%= link_to 'Show', @<%= singular_name %> %> | -<%%= link_to 'Back', <%= plural_name %>_path %> +<%%= link_to 'Show', @<%= singular_table_name %> %> | +<%%= link_to 'Back', <%= index_helper %>_path %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index d30d306d42..4c46db4d67 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -1,4 +1,4 @@ -<h1>Listing <%= plural_name %></h1> +<h1>Listing <%= plural_table_name %></h1> <table> <tr> @@ -10,18 +10,18 @@ <th></th> </tr> -<%% @<%= plural_name %>.each do |<%= singular_name %>| %> +<%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %> <tr> <% for attribute in attributes -%> - <td><%%= <%= singular_name %>.<%= attribute.name %> %></td> + <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td> <% end -%> - <td><%%= link_to 'Show', <%= singular_name %> %></td> - <td><%%= link_to 'Edit', edit_<%= singular_name %>_path(<%= singular_name %>) %></td> - <td><%%= link_to 'Destroy', <%= singular_name %>, :confirm => 'Are you sure?', :method => :delete %></td> + <td><%%= link_to 'Show', <%= singular_table_name %> %></td> + <td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td> + <td><%%= link_to 'Destroy', <%= singular_table_name %>, :confirm => 'Are you sure?', :method => :delete %></td> </tr> <%% end %> </table> <br /> -<%%= link_to 'New <%= human_name %>', new_<%= singular_name %>_path %> +<%%= link_to 'New <%= human_name %>', new_<%= singular_table_name %>_path %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb index 9a1c489331..02ae4d015e 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb @@ -1,5 +1,5 @@ -<h1>New <%= singular_name %></h1> +<h1>New <%= singular_table_name %></h1> <%%= render 'form' %> -<%%= link_to 'Back', <%= plural_name %>_path %> +<%%= link_to 'Back', <%= index_helper %>_path %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb index 6b3518717a..c0e5ccff1e 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb @@ -3,10 +3,10 @@ <% for attribute in attributes -%> <p> <b><%= attribute.human_name %>:</b> - <%%= @<%= singular_name %>.<%= attribute.name %> %> + <%%= @<%= singular_table_name %>.<%= attribute.name %> %> </p> <% end -%> -<%%= link_to 'Edit', edit_<%= singular_name %>_path(@<%= singular_name %>) %> | -<%%= link_to 'Back', <%= plural_name %>_path %> +<%%= link_to 'Edit', edit_<%= singular_table_name %>_path(@<%= singular_table_name %>) %> | +<%%= link_to 'Back', <%= index_helper %>_path %> diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 8d1dfbd947..44f831e6f3 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -46,6 +46,30 @@ module Rails end end + def uncountable? + singular_name == plural_name + end + + def index_helper + uncountable? ? "#{plural_table_name}_index" : plural_table_name + end + + def singular_table_name + @singular_table_name ||= table_name.singularize + end + + def plural_table_name + @plural_table_name ||= table_name.pluralize + end + + def plural_file_name + @plural_file_name ||= file_name.pluralize + end + + def route_url + @route_url ||= class_path.collect{|dname| "/" + dname }.join('') + "/" + plural_file_name + end + # Tries to retrieve the application name or simple return application. def application_name if defined?(Rails) && Rails.application diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 0b922a89c0..a108968b97 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -32,8 +32,9 @@ gem '<%= gem_for_database %>'<% if require_for_database %>, :require => '<%= req # gem 'sqlite3-ruby', :require => 'sqlite3' # gem 'aws-s3', :require => 'aws/s3' -# Bundle gems for certain environments: -# gem 'rspec', :group => :test -# group :test do +# Bundle gems for the local environment. Make sure to +# put test-only gems in this group so their generators +# and rake tasks are available in development mode: +# group :development, :test do # gem 'webrat' # end diff --git a/railties/lib/rails/generators/rails/app/templates/README b/railties/lib/rails/generators/rails/app/templates/README index 9ec6db6d71..e2764dee03 100644 --- a/railties/lib/rails/generators/rails/app/templates/README +++ b/railties/lib/rails/generators/rails/app/templates/README @@ -37,7 +37,7 @@ link:files/vendor/rails/actionpack/README.html. 3. Go to http://localhost:3000/ and you'll see: "Welcome aboard: You're riding the Rails!" -4. Follow the guidelines to start developing your application. You can find +4. Follow the guidelines to start developing your application. You can find the following resources handy: * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html @@ -71,13 +71,13 @@ The result will be a message in your log file along the lines of: More information on how to use the logger is at http://www.ruby-doc.org/core/ -Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are +Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are several books available online as well: * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) -These two books will bring you up to speed on the Ruby language and also on +These two books will bring you up to speed on the Ruby language and also on programming in general. @@ -199,7 +199,7 @@ app/controllers ApplicationController which itself descends from ActionController::Base. app/models - Holds models that should be named like post.rb. Models descend from + Holds models that should be named like post.rb. Models descend from ActiveRecord::Base by default. app/views diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile index 13f1f9fa41..13f1f9fa41 100755..100644 --- a/railties/lib/rails/generators/rails/app/templates/Rakefile +++ b/railties/lib/rails/generators/rails/app/templates/Rakefile diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb index 0066e2b0c2..031466cb86 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -22,7 +22,7 @@ module <%= app_const_base %> # -- all .rb files in that directory are automatically loaded. # Add additional load paths for your own custom dirs - # config.load_paths += %W( #{config.root}/extras ) + # config.autoload_paths += %W( #{config.root}/extras ) # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml index 2784a949fb..df5ef33064 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml @@ -32,40 +32,52 @@ # please refer to the latest documents at http://rubyforge.org/docman/?group_id=2361 development: - adapter: ibm_db - username: db2inst1 + adapter: ibm_db + username: db2inst1 password: - database: <%= app_name[0,4] %>_dev - #schema: db2inst1 - #host: localhost - #port: 50000 - #account: my_account - #app_user: my_app_user + database: <%= app_name[0,4] %>_dev + #schema: db2inst1 + #host: localhost + #port: 50000 + #account: my_account + #app_user: my_app_user #application: my_application #workstation: my_workstation + #security: SSL + #timeout: 10 + #authentication: SERVER + #parameterized: false test: - adapter: ibm_db - username: db2inst1 + adapter: ibm_db + username: db2inst1 password: - database: <%= app_name[0,4] %>_tst - #schema: db2inst1 - #host: localhost - #port: 50000 - #account: my_account - #app_user: my_app_user + database: <%= app_name[0,4] %>_tst + #schema: db2inst1 + #host: localhost + #port: 50000 + #account: my_account + #app_user: my_app_user #application: my_application #workstation: my_workstation + #security: SSL + #timeout: 10 + #authentication: SERVER + #parameterized: false production: - adapter: ibm_db - username: db2inst1 + adapter: ibm_db + username: db2inst1 password: - database: <%= app_name[0,8] %> - #schema: db2inst1 - #host: localhost - #port: 50000 - #account: my_account - #app_user: my_app_user + database: <%= app_name[0,8] %> + #schema: db2inst1 + #host: localhost + #port: 50000 + #account: my_account + #app_user: my_app_user #application: my_application #workstation: my_workstation + #security: SSL + #timeout: 10 + #authentication: SERVER + #parameterized: false
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml index 6bf2f7b1fd..ffc8a0a8cb 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -52,7 +52,7 @@ production: database: <%= app_name %>_production pool: 5 username: root - password: + password: <% if mysql_socket -%> socket: <%= mysql_socket %> <% else -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml index a1883f6256..f99ee937f3 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml @@ -36,4 +36,4 @@ production: adapter: oracle database: <%= app_name %>_production username: <%= app_name %> - password: + password: diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index f0e917dd96..99758dfcf7 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -16,4 +16,7 @@ # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = false + + # Print deprecation notices to the Rails logger + config.active_support.deprecation = :log end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index b9fb13b640..500fc4d860 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -43,4 +43,7 @@ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found) config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners + config.active_support.deprecation = :notify end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt index beb28e2229..26cdef071a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt @@ -29,4 +29,7 @@ # This is necessary if your schema can't be completely dumped by the schema dumper, # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql + + # Print deprecation notices to the stderr + config.active_support.deprecation = :stderr end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb index d531b8bb82..9e8b0131f8 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb @@ -1,6 +1,6 @@ # Be sure to restart your server when you modify this file. -# Add new inflection rules using the following format +# Add new inflection rules using the following format # (all these examples are active by default): # ActiveSupport::Inflector.inflections do |inflect| # inflect.plural /^(ox)$/i, '\1en' diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt index c2fa31aadb..22aa576f5d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt @@ -2,6 +2,6 @@ # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! -# Make sure the secret is at least 30 characters and all random, +# Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. Rails.application.config.secret_token = '<%= app_secret %>' diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb index d6c0365c04..d42cf3bfdf 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb @@ -1,4 +1,4 @@ -<%= app_const %>.routes.draw do |map| +<%= app_const %>.routes.draw do # The priority is based upon order of creation: # first created -> highest priority. diff --git a/railties/lib/rails/generators/rails/app/templates/public/javascripts/rails.js b/railties/lib/rails/generators/rails/app/templates/public/javascripts/rails.js index c5fa02ae35..4283ed8982 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/javascripts/rails.js +++ b/railties/lib/rails/generators/rails/app/templates/public/javascripts/rails.js @@ -1,87 +1,148 @@ -document.observe("dom:loaded", function() { +(function() { + // Technique from Juriy Zaytsev + // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ + function isEventSupported(eventName) { + var el = document.createElement('div'); + eventName = 'on' + eventName; + var isSupported = (eventName in el); + if (!isSupported) { + el.setAttribute(eventName, 'return;'); + isSupported = typeof el[eventName] == 'function'; + } + el = null; + return isSupported; + } + + function isForm(element) { + return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM' + } + + function isInput(element) { + if (Object.isElement(element)) { + var name = element.nodeName.toUpperCase() + return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA' + } + else return false + } + + var submitBubbles = isEventSupported('submit'), + changeBubbles = isEventSupported('change') + + if (!submitBubbles || !changeBubbles) { + // augment the Event.Handler class to observe custom events when needed + Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap( + function(init, element, eventName, selector, callback) { + init(element, eventName, selector, callback) + // is the handler being attached to an element that doesn't support this event? + if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) || + (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) { + // "submit" => "emulated:submit" + this.eventName = 'emulated:' + this.eventName + } + } + ) + } + + if (!submitBubbles) { + // discover forms on the page by observing focus events which always bubble + document.on('focusin', 'form', function(focusEvent, form) { + // special handler for the real "submit" event (one-time operation) + if (!form.retrieve('emulated:submit')) { + form.on('submit', function(submitEvent) { + var emulated = form.fire('emulated:submit', submitEvent, true) + // if custom event received preventDefault, cancel the real one too + if (emulated.returnValue === false) submitEvent.preventDefault() + }) + form.store('emulated:submit', true) + } + }) + } + + if (!changeBubbles) { + // discover form inputs on the page + document.on('focusin', 'input, select, texarea', function(focusEvent, input) { + // special handler for real "change" events + if (!input.retrieve('emulated:change')) { + input.on('change', function(changeEvent) { + input.fire('emulated:change', changeEvent, true) + }) + input.store('emulated:change', true) + } + }) + } + function handleRemote(element) { var method, url, params; + var event = element.fire("ajax:before"); + if (event.stopped) return false; + if (element.tagName.toLowerCase() === 'form') { method = element.readAttribute('method') || 'post'; url = element.readAttribute('action'); - params = element.serialize(true); + params = element.serialize(); } else { method = element.readAttribute('data-method') || 'get'; url = element.readAttribute('href'); params = {}; } - var event = element.fire("ajax:before"); - if (event.stopped) return false; - new Ajax.Request(url, { method: method, parameters: params, - asynchronous: true, evalScripts: true, - onLoading: function(request) { element.fire("ajax:loading", {request: request}); }, - onLoaded: function(request) { element.fire("ajax:loaded", {request: request}); }, - onInteractive: function(request) { element.fire("ajax:interactive", {request: request}); }, - onComplete: function(request) { element.fire("ajax:complete", {request: request}); }, - onSuccess: function(request) { element.fire("ajax:success", {request: request}); }, - onFailure: function(request) { element.fire("ajax:failure", {request: request}); } + onComplete: function(request) { element.fire("ajax:complete", request); }, + onSuccess: function(request) { element.fire("ajax:success", request); }, + onFailure: function(request) { element.fire("ajax:failure", request); } }); element.fire("ajax:after"); } function handleMethod(element) { - var method, url, token_name, token; - - method = element.readAttribute('data-method'); - url = element.readAttribute('href'); - csrf_param = $$('meta[name=csrf-param]').first(); - csrf_token = $$('meta[name=csrf-token]').first(); + var method = element.readAttribute('data-method'), + url = element.readAttribute('href'), + csrf_param = $$('meta[name=csrf-param]')[0], + csrf_token = $$('meta[name=csrf-token]')[0]; var form = new Element('form', { method: "POST", action: url, style: "display: none;" }); - element.parentNode.appendChild(form); + element.parentNode.insert(form); - if (method != 'post') { + if (method !== 'post') { var field = new Element('input', { type: 'hidden', name: '_method', value: method }); - form.appendChild(field); + form.insert(field); } if (csrf_param) { - var param = csrf_param.readAttribute('content'); - var token = csrf_token.readAttribute('content'); - var field = new Element('input', { type: 'hidden', name: param, value: token }); - form.appendChild(field); + var param = csrf_param.readAttribute('content'), + token = csrf_token.readAttribute('content'), + field = new Element('input', { type: 'hidden', name: param, value: token }); + form.insert(field); } form.submit(); } - $(document.body).observe("click", function(event) { - var message = event.findElement().readAttribute('data-confirm'); - if (message && !confirm(message)) { - event.stop(); - return false; - } - var element = event.findElement("a[data-remote]"); - if (element) { - handleRemote(element); - event.stop(); - return true; - } + document.on("click", "*[data-confirm]", function(event, element) { + var message = element.readAttribute('data-confirm'); + if (!confirm(message)) event.stop(); + }); - var element = event.findElement("a[data-method]"); - if (element) { - handleMethod(element); - event.stop(); - return true; - } + document.on("click", "a[data-remote]", function(event, element) { + if (event.stopped) return; + handleRemote(element); + event.stop(); + }); + + document.on("click", "a[data-method]", function(event, element) { + if (event.stopped) return; + handleMethod(element); + event.stop(); }); - // TODO: I don't think submit bubbles in IE - $(document.body).observe("submit", function(event) { + document.on("submit", function(event) { var element = event.findElement(), message = element.readAttribute('data-confirm'); if (message && !confirm(message)) { @@ -103,16 +164,12 @@ document.observe("dom:loaded", function() { } }); - $(document.body).observe("ajax:after", function(event) { - var element = event.findElement(); - - if (element.tagName.toLowerCase() === 'form') { - var inputs = element.select("input[type=submit][disabled=true][data-disable-with]"); - inputs.each(function(input) { - input.value = input.readAttribute('data-original-value'); - input.writeAttribute('data-original-value', null); - input.disabled = false; - }); - } + document.on("ajax:after", "form", function(event, element) { + var inputs = element.select("input[type=submit][disabled=true][data-disable-with]"); + inputs.each(function(input) { + input.value = input.readAttribute('data-original-value'); + input.removeAttribute('data-original-value'); + input.disabled = false; + }); }); -});
\ No newline at end of file +})(); diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE index db98a2dd1b..67f76aad01 100644 --- a/railties/lib/rails/generators/rails/model/USAGE +++ b/railties/lib/rails/generators/rails/model/USAGE @@ -40,6 +40,6 @@ Examples: Module: app/models/admin.rb Model: app/models/admin/account.rb Test: test/unit/admin/account_test.rb - Fixtures: test/fixtures/admin_accounts.yml + Fixtures: test/fixtures/admin/accounts.yml Migration: db/migrate/XXX_add_admin_accounts.rb diff --git a/railties/lib/rails/generators/rails/resource/resource_generator.rb b/railties/lib/rails/generators/rails/resource/resource_generator.rb index 8a46708009..fc070026d6 100644 --- a/railties/lib/rails/generators/rails/resource/resource_generator.rb +++ b/railties/lib/rails/generators/rails/resource/resource_generator.rb @@ -14,23 +14,13 @@ module Rails class_option :actions, :type => :array, :banner => "ACTION ACTION", :default => [], :desc => "Actions for the resource controller" - class_option :singleton, :type => :boolean, :desc => "Supply to create a singleton controller" - def add_resource_route return if options[:actions].present? - route "resource#{:s unless options[:singleton]} :#{pluralize?(file_name)}" + route_config = class_path.collect{|namespace| "namespace :#{namespace} do " }.join(" ") + route_config << "resources :#{file_name.pluralize}" + route_config << " end" * class_path.size + route route_config end - - protected - - def pluralize?(name) - if options[:singleton] - name - else - name.pluralize - end - end - end end end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb index 49af2974cd..2271c6f9c1 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb @@ -10,8 +10,6 @@ module Rails class_option :orm, :banner => "NAME", :type => :string, :required => true, :desc => "ORM to generate the controller for" - class_option :singleton, :type => :boolean, :desc => "Supply to create a singleton controller" - def create_controller_files template 'controller.rb', File.join('app/controllers', class_path, "#{controller_file_name}_controller.rb") end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb index bbdce669dc..b21340f755 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -1,53 +1,51 @@ class <%= controller_class_name %>Controller < ApplicationController -<% unless options[:singleton] -%> - # GET /<%= table_name %> - # GET /<%= table_name %>.xml + # GET <%= route_url %> + # GET <%= route_url %>.xml def index - @<%= table_name %> = <%= orm_class.all(class_name) %> + @<%= plural_table_name %> = <%= orm_class.all(class_name) %> respond_to do |format| format.html # index.html.erb - format.xml { render :xml => @<%= table_name %> } + format.xml { render :xml => @<%= plural_table_name %> } end end -<% end -%> - # GET /<%= table_name %>/1 - # GET /<%= table_name %>/1.xml + # GET <%= route_url %>/1 + # GET <%= route_url %>/1.xml def show - @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> respond_to do |format| format.html # show.html.erb - format.xml { render :xml => @<%= file_name %> } + format.xml { render :xml => @<%= singular_table_name %> } end end - # GET /<%= table_name %>/new - # GET /<%= table_name %>/new.xml + # GET <%= route_url %>/new + # GET <%= route_url %>/new.xml def new - @<%= file_name %> = <%= orm_class.build(class_name) %> + @<%= singular_table_name %> = <%= orm_class.build(class_name) %> respond_to do |format| format.html # new.html.erb - format.xml { render :xml => @<%= file_name %> } + format.xml { render :xml => @<%= singular_table_name %> } end end - # GET /<%= table_name %>/1/edit + # GET <%= route_url %>/1/edit def edit - @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> end - # POST /<%= table_name %> - # POST /<%= table_name %>.xml + # POST <%= route_url %> + # POST <%= route_url %>.xml def create - @<%= file_name %> = <%= orm_class.build(class_name, "params[:#{file_name}]") %> + @<%= singular_table_name %> = <%= orm_class.build(class_name, "params[:#{singular_table_name}]") %> respond_to do |format| if @<%= orm_instance.save %> - format.html { redirect_to(@<%= file_name %>, :notice => '<%= human_name %> was successfully created.') } - format.xml { render :xml => @<%= file_name %>, :status => :created, :location => @<%= file_name %> } + format.html { redirect_to(@<%= singular_table_name %>, :notice => '<%= human_name %> was successfully created.') } + format.xml { render :xml => @<%= singular_table_name %>, :status => :created, :location => @<%= singular_table_name %> } else format.html { render :action => "new" } format.xml { render :xml => @<%= orm_instance.errors %>, :status => :unprocessable_entity } @@ -55,14 +53,14 @@ class <%= controller_class_name %>Controller < ApplicationController end end - # PUT /<%= table_name %>/1 - # PUT /<%= table_name %>/1.xml + # PUT <%= route_url %>/1 + # PUT <%= route_url %>/1.xml def update - @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> respond_to do |format| - if @<%= orm_instance.update_attributes("params[:#{file_name}]") %> - format.html { redirect_to(@<%= file_name %>, :notice => '<%= human_name %> was successfully updated.') } + if @<%= orm_instance.update_attributes("params[:#{singular_table_name}]") %> + format.html { redirect_to(@<%= singular_table_name %>, :notice => '<%= human_name %> was successfully updated.') } format.xml { head :ok } else format.html { render :action => "edit" } @@ -71,14 +69,14 @@ class <%= controller_class_name %>Controller < ApplicationController end end - # DELETE /<%= table_name %>/1 - # DELETE /<%= table_name %>/1.xml + # DELETE <%= route_url %>/1 + # DELETE <%= route_url %>/1.xml def destroy - @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> @<%= orm_instance.destroy %> respond_to do |format| - format.html { redirect_to(<%= table_name %>_url) } + format.html { redirect_to(<%= index_helper %>_url) } format.xml { head :ok } end end diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb index 3a98a8f9c1..829f4b200a 100644 --- a/railties/lib/rails/generators/resource_helpers.rb +++ b/railties/lib/rails/generators/resource_helpers.rb @@ -72,7 +72,7 @@ module Rails end # Initialize ORM::Generators::ActiveModel to access instance methods. - def orm_instance(name=file_name) + def orm_instance(name=singular_table_name) @orm_instance ||= @orm_class.new(name) end end diff --git a/railties/lib/rails/generators/test_unit/model/model_generator.rb b/railties/lib/rails/generators/test_unit/model/model_generator.rb index 609b815683..c1dd535dd3 100644 --- a/railties/lib/rails/generators/test_unit/model/model_generator.rb +++ b/railties/lib/rails/generators/test_unit/model/model_generator.rb @@ -16,7 +16,7 @@ module TestUnit def create_fixture_file if options[:fixture] && options[:fixture_replacement].nil? - template 'fixtures.yml', File.join('test/fixtures', "#{table_name}.yml") + template 'fixtures.yml', File.join('test/fixtures', class_path, "#{plural_file_name}.yml") end end end diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb index c0315c7fe6..f7e907a017 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -6,7 +6,6 @@ module TestUnit class ScaffoldGenerator < Base include Rails::Generators::ResourceHelpers - class_option :singleton, :type => :boolean, :desc => "Supply to create a singleton controller" check_class_collision :suffix => "ControllerTest" def create_test_files diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb index 4f8ddbffcf..f23e495450 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb @@ -2,50 +2,48 @@ require 'test_helper' class <%= controller_class_name %>ControllerTest < ActionController::TestCase setup do - @<%= file_name %> = <%= table_name %>(:one) + @<%= singular_table_name %> = <%= table_name %>(:one) end -<% unless options[:singleton] -%> test "should get index" do get :index assert_response :success assert_not_nil assigns(:<%= table_name %>) end -<% end -%> test "should get new" do get :new assert_response :success end - test "should create <%= file_name %>" do + test "should create <%= singular_table_name %>" do assert_difference('<%= class_name %>.count') do - post :create, :<%= file_name %> => @<%= file_name %>.attributes + post :create, :<%= singular_table_name %> => @<%= singular_table_name %>.attributes end - assert_redirected_to <%= file_name %>_path(assigns(:<%= file_name %>)) + assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>)) end - test "should show <%= file_name %>" do - get :show, :id => @<%= file_name %>.to_param + test "should show <%= singular_table_name %>" do + get :show, :id => @<%= singular_table_name %>.to_param assert_response :success end test "should get edit" do - get :edit, :id => @<%= file_name %>.to_param + get :edit, :id => @<%= singular_table_name %>.to_param assert_response :success end - test "should update <%= file_name %>" do - put :update, :id => @<%= file_name %>.to_param, :<%= file_name %> => @<%= file_name %>.attributes - assert_redirected_to <%= file_name %>_path(assigns(:<%= file_name %>)) + test "should update <%= singular_table_name %>" do + put :update, :id => @<%= singular_table_name %>.to_param, :<%= singular_table_name %> => @<%= singular_table_name %>.attributes + assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>)) end - test "should destroy <%= file_name %>" do + test "should destroy <%= singular_table_name %>" do assert_difference('<%= class_name %>.count', -1) do - delete :destroy, :id => @<%= file_name %>.to_param + delete :destroy, :id => @<%= singular_table_name %>.to_param end - assert_redirected_to <%= table_name %>_path + assert_redirected_to <%= index_helper %>_path end end diff --git a/railties/lib/rails/initializable.rb b/railties/lib/rails/initializable.rb index 9a82e051e7..686a2dc0cb 100644 --- a/railties/lib/rails/initializable.rb +++ b/railties/lib/rails/initializable.rb @@ -39,11 +39,6 @@ module Rails select { |i| i.before == initializer.name || i.name == initializer.after }.each(&block) end - def initialize(initializers = []) - super(initializers) - replace(tsort) - end - def +(other) Collection.new(to_a + other.to_a) end @@ -51,7 +46,7 @@ module Rails def run_initializers(*args) return if instance_variable_defined?(:@ran) - initializers.each do |initializer| + initializers.tsort.each do |initializer| initializer.run(*args) end @ran = true @@ -63,7 +58,7 @@ module Rails module ClassMethods def initializers - @initializers ||= [] + @initializers ||= Collection.new end def initializers_chain @@ -84,14 +79,6 @@ module Rails opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] } initializers << Initializer.new(name, nil, opts, &blk) end - - def run_initializers(*args) - return if @ran - initializers_chain.each do |initializer| - instance_exec(*args, &initializer.block) - end - @ran = true - end end end -end
\ No newline at end of file +end diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index 1c9e308631..7a65188a9a 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -25,9 +25,7 @@ module Rails def initialize(path) raise if path.is_a?(Array) - @children = {} - @path = path @root = self @all_paths = [] @@ -38,14 +36,18 @@ module Rails @all_paths end - def load_once - filter_by(:load_once?) + def autoload_once + filter_by(:autoload_once?) end def eager_load filter_by(:eager_load?) end + def autoload_paths + filter_by(:autoload?) + end + def load_paths filter_by(:load_path?) end @@ -61,15 +63,17 @@ module Rails protected def filter_by(constraint) - all_paths.map do |path| + all = [] + all_paths.each do |path| if path.send(constraint) paths = path.paths paths -= path.children.values.map { |p| p.send(constraint) ? [] : p.paths }.flatten - paths - else - [] + all.concat(paths) end - end.flatten.uniq.select { |p| File.exists?(p) } + end + all.uniq! + all.reject! { |p| !File.exists?(p) } + all end end @@ -80,15 +84,16 @@ module Rails attr_accessor :glob def initialize(root, *paths) - @options = paths.last.is_a?(::Hash) ? paths.pop : {} + options = paths.last.is_a?(::Hash) ? paths.pop : {} @children = {} @root = root @paths = paths.flatten - @glob = @options.delete(:glob) + @glob = options[:glob] - @load_once = @options[:load_once] - @eager_load = @options[:eager_load] - @load_path = @options[:load_path] || @eager_load || @load_once + autoload_once! if options[:autoload_once] + eager_load! if options[:eager_load] + autoload! if options[:autoload] + load_path! if options[:load_path] @root.all_paths << self end @@ -111,24 +116,30 @@ module Rails @paths.concat paths end - def load_once! - @load_once = true - @load_path = true + def autoload_once! + @autoload_once = true end - def load_once? - @load_once + def autoload_once? + @autoload_once end def eager_load! @eager_load = true - @load_path = true end def eager_load? @eager_load end + def autoload! + @autoload = true + end + + def autoload? + @autoload + end + def load_path! @load_path = true end diff --git a/railties/lib/rails/plugin.rb b/railties/lib/rails/plugin.rb index fcdd099135..be229cc9a2 100644 --- a/railties/lib/rails/plugin.rb +++ b/railties/lib/rails/plugin.rb @@ -61,6 +61,16 @@ module Rails @config ||= Engine::Configuration.new end + initializer :handle_lib_autoload, :before => :set_load_path do |app| + paths = if app.config.reload_plugins + config.autoload_paths + else + config.autoload_once_paths + end + + paths.concat config.paths.lib.to_a + end + initializer :load_init_rb, :before => :load_config_initializers do |app| files = %w(rails/init.rb init.rb).map { |path| File.expand_path path, root } if initrb = files.find { |path| File.file? path } @@ -77,11 +87,5 @@ module Rails raise "\"#{name}\" is a Railtie/Engine and cannot be installed as plugin" end end - - protected - - def reloadable?(app) - app.config.reload_plugins - end end end diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index 73e9af3b41..b3dc1f894c 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -1,10 +1,9 @@ -require 'rails/log_subscriber' require 'active_support/core_ext/time/conversions' module Rails module Rack # Log the request started and flush all loggers after it. - class Logger < Rails::LogSubscriber + class Logger < ActiveSupport::LogSubscriber def initialize(app) @app = app end @@ -16,20 +15,19 @@ module Rails after_dispatch(env) end - protected + protected - def before_dispatch(env) - request = ActionDispatch::Request.new(env) - path = request.fullpath + def before_dispatch(env) + request = ActionDispatch::Request.new(env) + path = request.fullpath - info "\n\nStarted #{env["REQUEST_METHOD"]} \"#{path}\" " \ - "for #{request.ip} at #{Time.now.to_default_s}" - end - - def after_dispatch(env) - Rails::LogSubscriber.flush_all! - end + info "\n\nStarted #{env["REQUEST_METHOD"]} \"#{path}\" " \ + "for #{request.ip} at #{Time.now.to_default_s}" + end + def after_dispatch(env) + ActiveSupport::LogSubscriber.flush_all! + end end end end diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index ad776933f2..dbdbfea509 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -114,36 +114,6 @@ module Rails # end # end # - # == Adding your subscriber - # - # Since version 3.0, Rails ships with a notification system which is used for several - # purposes, including logging. If you are sending notifications in your Railtie, you may - # want to add a subscriber to consume such notifications for logging purposes. - # - # The subscriber is added under the railtie_name namespace and only consumes notifications - # under the given namespace. For example, let's suppose your railtie is publishing the - # following "something_expensive" instrumentation: - # - # ActiveSupport::Notifications.instrument "my_railtie.something_expensive" do - # # something expensive - # end - # - # You can log this instrumentation with your own Rails::Subscriber: - # - # class MyRailtie::Subscriber < Rails::Subscriber - # def something_expensive(event) - # info("Something expensive took %.1fms" % event.duration) - # end - # end - # - # By registering it: - # - # class MyRailtie < Railtie - # subscriber :my_gem, MyRailtie::Subscriber.new - # end - # - # Take a look in Rails::Subscriber docs for more information. - # # == Application, Plugin and Engine # # A Rails::Engine is nothing more than a Railtie with some initializers already set. @@ -176,8 +146,8 @@ module Rails ActiveSupport::Deprecation.warn "railtie_name is deprecated and has no effect", caller end - def log_subscriber(name, log_subscriber) - Rails::LogSubscriber.add(name, log_subscriber) + def log_subscriber(*) + ActiveSupport::Deprecation.warn "log_subscriber is deprecated and has no effect", caller end def rake_tasks(&blk) diff --git a/railties/lib/rails/webrick_server.rb b/railties/lib/rails/webrick_server.rb deleted file mode 100644 index f3b74c28d3..0000000000 --- a/railties/lib/rails/webrick_server.rb +++ /dev/null @@ -1,156 +0,0 @@ -# Donated by Florian Gross - -require 'webrick' -require 'cgi' -require 'stringio' -require 'dispatcher' - -include WEBrick - -class CGI #:nodoc: - def stdinput - @stdin || $stdin - end - - def env_table - @env_table || ENV - end - - def initialize(type = "query", table = nil, stdin = nil) - @env_table, @stdin = table, stdin - - if defined?(MOD_RUBY) && !ENV.key?("GATEWAY_INTERFACE") - Apache.request.setup_cgi_env - end - - extend QueryExtension - @multipart = false - if defined?(CGI_PARAMS) - warn "do not use CGI_PARAMS and CGI_COOKIES" - @params = CGI_PARAMS.dup - @cookies = CGI_COOKIES.dup - else - initialize_query() # set @params, @cookies - end - @output_cookies = nil - @output_hidden = nil - end -end - -# A custom dispatch servlet for use with WEBrick. It dispatches requests -# (using the Rails Dispatcher) to the appropriate controller/action. By default, -# it restricts WEBrick to a managing a single Rails request at a time, but you -# can change this behavior by setting ActionController::Base.allow_concurrency -# to true. -class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet - # Start the WEBrick server with the given options, mounting the - # DispatchServlet at <tt>/</tt>. - def self.dispatch(options = {}) - Socket.do_not_reverse_lookup = true # patch for OS X - - params = { :Port => options[:port].to_i, - :ServerType => options[:server_type], - :BindAddress => options[:ip] } - params[:MimeTypes] = options[:mime_types] if options[:mime_types] - - server = WEBrick::HTTPServer.new(params) - server.mount('/', DispatchServlet, options) - - trap("INT") { server.shutdown } - server.start - end - - def initialize(server, options) #:nodoc: - @server_options = options - @file_handler = WEBrick::HTTPServlet::FileHandler.new(server, options[:server_root]) - # Change to the Rails.root, since Webrick::Daemon.start does a Dir::cwd("/") - # OPTIONS['working_directory'] is an absolute path of the Rails.root, set in railties/lib/commands/servers/webrick.rb - Dir.chdir(OPTIONS['working_directory']) if defined?(OPTIONS) && File.directory?(OPTIONS['working_directory']) - super - end - - def service(req, res) #:nodoc: - unless handle_file(req, res) - unless handle_dispatch(req, res) - raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." - end - end - end - - def handle_file(req, res) #:nodoc: - begin - req = req.dup - path = req.path.dup - - # Add .html if the last path piece has no . in it - path << '.html' if path != '/' && (%r{(^|/)[^./]+$} =~ path) - path.gsub!('+', ' ') # Unescape + since FileHandler doesn't do so. - - req.instance_variable_set(:@path_info, path) # Set the modified path... - - @file_handler.send(:service, req, res) - return true - rescue HTTPStatus::PartialContent, HTTPStatus::NotModified => err - res.set_error(err) - return true - rescue => err - return false - end - end - - def handle_dispatch(req, res, origin = nil) #:nodoc: - data = StringIO.new - Dispatcher.dispatch( - CGI.new("query", create_env_table(req, origin), StringIO.new(req.body || "")), - ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, - data - ) - - header, body = extract_header_and_body(data) - - set_charset(header) - assign_status(res, header) - res.cookies.concat(header.delete('set-cookie') || []) - header.each { |key, val| res[key] = val.join(", ") } - - res.body = body - return true - rescue => err - p err, err.backtrace - return false - end - - private - def create_env_table(req, origin) - env = req.meta_vars.clone - env.delete "SCRIPT_NAME" - env["QUERY_STRING"] = req.request_uri.query - env["REQUEST_URI"] = origin if origin - return env - end - - def extract_header_and_body(data) - data.rewind - data = data.read - - raw_header, body = *data.split(/^[\xd\xa]{2}/on, 2) - header = WEBrick::HTTPUtils::parse_header(raw_header) - - return header, body - end - - def set_charset(header) - ct = header["content-type"] - if ct.any? { |x| x =~ /^text\// } && ! ct.any? { |x| x =~ /charset=/ } - ch = @server_options[:charset] || "UTF-8" - ct.find { |x| x =~ /^text\// } << ("; charset=" + ch) - end - end - - def assign_status(res, header) - if /^(\d+)/ =~ header['status'][0] - res.status = $1.to_i - header.delete('status') - end - end -end diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index d04a2aa1f3..a05bae5dcc 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -1,5 +1,3 @@ -ORIG_ARGV = ARGV.dup - require File.expand_path("../../../load_paths", __FILE__) require 'stringio' diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 35ea2729d3..7269a7c5a8 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -28,8 +28,10 @@ module ApplicationTests RUBY require "#{app_path}/config/environment" - ActionController::Base.view_paths.include?(File.expand_path("app/views", app_path)) - ActionMailer::Base.view_paths.include?(File.expand_path("app/views", app_path)) + + expanded_path = File.expand_path("app/views", app_path) + assert_equal ActionController::Base.view_paths[0].to_s, expanded_path + assert_equal ActionMailer::Base.view_paths[0].to_s, expanded_path end test "allows me to configure default url options for ActionMailer" do @@ -40,7 +42,7 @@ module ApplicationTests RUBY require "#{app_path}/config/environment" - assert "test.rails", ActionMailer::Base.default_url_options[:host] + assert_equal "test.rails", ActionMailer::Base.default_url_options[:host] end # AS @@ -57,7 +59,7 @@ module ApplicationTests Dir.chdir("#{app_path}/app") do require "#{app_path}/config/environment" - assert_raises(NoMethodError) { [1,2,3].sample } + assert_raises(NoMethodError) { [1,2,3].forty_two } end end diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index 99b2d86013..a1fcba3310 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -8,48 +8,143 @@ module ApplicationTests build_app boot_rails FileUtils.rm_rf "#{app_path}/config/environments" + require "rails/all" end - # i18n + def load_app + require "#{app_path}/config/environment" + end + + def app + @app ||= Rails::Application + end + + def assert_fallbacks(fallbacks) + fallbacks.each do |locale, expected| + actual = I18n.fallbacks[locale] + assert_equal expected, actual, "expected fallbacks for #{locale.inspect} to be #{expected.inspect}, but were #{actual.inspect}" + end + end + + def assert_no_fallbacks + assert !I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) + end + + # Locales test "setting another default locale" do add_to_config <<-RUBY - config.root = "#{app_path}" config.i18n.default_locale = :de RUBY - require "#{app_path}/config/environment" + load_app assert_equal :de, I18n.default_locale end + # Load paths test "no config locales dir present should return empty load path" do FileUtils.rm_rf "#{app_path}/config/locales" - add_to_config <<-RUBY - config.root = "#{app_path}" - RUBY - require "#{app_path}/config/environment" - + load_app assert_equal [], Rails.application.config.i18n.load_path end - test "config locales dir present should be added to load path" do + test "locale files should be added to the load path" do + app_file "config/another_locale.yml", "" + add_to_config <<-RUBY - config.root = "#{app_path}" + config.i18n.load_path << config.root.join("config/another_locale.yml").to_s RUBY - require "#{app_path}/config/environment" - assert_equal ["#{app_path}/config/locales/en.yml"], Rails.application.config.i18n.load_path + load_app + assert_equal [ + "#{app_path}/config/locales/en.yml", "#{app_path}/config/another_locale.yml" + ], Rails.application.config.i18n.load_path + + assert I18n.load_path.include?("#{app_path}/config/locales/en.yml") + assert I18n.load_path.include?("#{app_path}/config/another_locale.yml") end - test "config defaults should be added with config settings" do + test "locales are reloaded if they change between requests" do add_to_config <<-RUBY - config.root = "#{app_path}" - config.i18n.load_path << "my/other/locale.yml" + config.cache_classes = false RUBY - require "#{app_path}/config/environment" - assert_equal [ - "#{app_path}/config/locales/en.yml", "my/other/locale.yml" - ], Rails.application.config.i18n.load_path + app_file "config/locales/en.yml", <<-YAML +en: + foo: "1" + YAML + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do |map| + match '/i18n', :to => lambda { |env| [200, {}, [I18n.t(:foo)]] } + end + RUBY + + require 'rack/test' + extend Rack::Test::Methods + load_app + + get "/i18n" + assert_equal "1", last_response.body + + app_file "config/locales/en.yml", <<-YAML +en: + foo: "2" + YAML + + get "/i18n" + assert_equal "2", last_response.body + end + + # Fallbacks + test "not using config.i18n.fallbacks does not initialize I18n.fallbacks" do + I18n.backend = Class.new { include I18n::Backend::Base }.new + load_app + assert_no_fallbacks + end + + test "config.i18n.fallbacks = true initializes I18n.fallbacks with default settings" do + I18n::Railtie.config.i18n.fallbacks = true + load_app + assert I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) + assert_fallbacks :de => [:de, :en] + end + + test "config.i18n.fallbacks = true initializes I18n.fallbacks with default settings even when backend changes" do + I18n::Railtie.config.i18n.fallbacks = true + I18n::Railtie.config.i18n.backend = Class.new { include I18n::Backend::Base }.new + load_app + assert I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) + assert_fallbacks :de => [:de, :en] + end + + test "config.i18n.fallbacks.defaults = [:'en-US'] initializes fallbacks with en-US as a fallback default" do + I18n::Railtie.config.i18n.fallbacks.defaults = [:'en-US'] + load_app + assert_fallbacks :de => [:de, :'en-US', :en] + end + + test "config.i18n.fallbacks.map = { :ca => :'es-ES' } initializes fallbacks with a mapping ca => es-ES" do + I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' } + load_app + assert_fallbacks :ca => [:ca, :"es-ES", :es, :en] + end + + test "[shortcut] config.i18n.fallbacks = [:'en-US'] initializes fallbacks with en-US as a fallback default" do + I18n::Railtie.config.i18n.fallbacks = [:'en-US'] + load_app + assert_fallbacks :de => [:de, :'en-US', :en] + end + + test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping de-AT => de-DE" do + I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' } + load_app + assert_fallbacks :ca => [:ca, :"es-ES", :es, :en] + end + + test "[shortcut] config.i18n.fallbacks = [:'en-US', { :ca => :'es-ES' }] initializes fallbacks with the given arguments" do + I18n::Railtie.config.i18n.fallbacks = [:'en-US', { :ca => :'es-ES' }] + load_app + assert_fallbacks :ca => [:ca, :"es-ES", :es, :'en-US', :en] end end end
\ No newline at end of file diff --git a/railties/test/application/initializers/load_path_test.rb b/railties/test/application/initializers/load_path_test.rb index d31915e129..714d62311d 100644 --- a/railties/test/application/initializers/load_path_test.rb +++ b/railties/test/application/initializers/load_path_test.rb @@ -19,7 +19,7 @@ module ApplicationTests assert $:.include?("#{app_path}/app/models") end - test "initializing an application adds lib path on inheritance hook" do + test "initializing an application allows to load code on lib path inside application class definitation" do app_file "lib/foo.rb", <<-RUBY module Foo; end RUBY diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb new file mode 100644 index 0000000000..340ce67511 --- /dev/null +++ b/railties/test/application/loading_test.rb @@ -0,0 +1,86 @@ +require 'isolation/abstract_unit' + +class LoadingTest < Test::Unit::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + end + + def app + @app ||= Rails.application + end + + def test_constants_in_app_are_autoloaded + app_file "app/models/post.rb", <<-MODEL + class Post < ActiveRecord::Base + validates_acceptance_of :title, :accept => "omg" + end + MODEL + + require "#{rails_root}/config/environment" + setup_ar! + + p = Post.create(:title => 'omg') + assert_equal 1, Post.count + assert_equal 'omg', p.title + p = Post.first + assert_equal 'omg', p.title + end + + def test_models_without_table_do_not_panic_on_scope_definitions_when_loaded + app_file "app/models/user.rb", <<-MODEL + class User < ActiveRecord::Base + default_scope where(:published => true) + end + MODEL + + require "#{rails_root}/config/environment" + setup_ar! + + User + end + + def test_descendants_are_cleaned_on_each_request_without_cache_classes + add_to_config <<-RUBY + config.cache_classes = false + RUBY + + app_file "app/models/post.rb", <<-MODEL + class Post < ActiveRecord::Base + end + MODEL + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do |map| + match '/load', :to => lambda { |env| [200, {}, Post.all] } + match '/unload', :to => lambda { |env| [200, {}, []] } + end + RUBY + + require 'rack/test' + extend Rack::Test::Methods + + require "#{rails_root}/config/environment" + setup_ar! + + assert_equal [], ActiveRecord::Base.descendants + get "/load" + assert_equal [Post], ActiveRecord::Base.descendants + get "/unload" + assert_equal [], ActiveRecord::Base.descendants + end + + protected + + def setup_ar! + ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(:version => 1) do + create_table :posts do |t| + t.string :title + end + end + end +end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 999f666a64..e66e81ea2c 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -83,6 +83,17 @@ module ApplicationTests assert_equal "Rack::Config", middleware.second end + test "RAILS_CACHE does not respond to middleware" do + add_to_config "config.cache_store = :memory_store" + boot! + assert_equal "Rack::Runtime", middleware.third + end + + test "RAILS_CACHE does respond to middleware" do + boot! + assert_equal "Rack::Runtime", middleware.fourth + end + test "insert middleware before" do add_to_config "config.middleware.insert_before ActionDispatch::Static, Rack::Config" boot! diff --git a/railties/test/application/model_initialization_test.rb b/railties/test/application/model_initialization_test.rb deleted file mode 100644 index 6a22f8d8df..0000000000 --- a/railties/test/application/model_initialization_test.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'isolation/abstract_unit' - -class PostTest < Test::Unit::TestCase - include ActiveSupport::Testing::Isolation - - def setup - build_app - boot_rails - end - - def test_reload_should_reload_constants - app_file "app/models/post.rb", <<-MODEL - class Post < ActiveRecord::Base - validates_acceptance_of :title, :accept => "omg" - end - MODEL - - require "#{rails_root}/config/environment" - ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:") - ActiveRecord::Migration.verbose = false - ActiveRecord::Schema.define(:version => 1) do - create_table :posts do |t| - t.string :title - end - end - - p = Post.create(:title => 'omg') - assert_equal 1, Post.count - assert_equal 'omg', p.title - p = Post.first - assert_equal 'omg', p.title - end -end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 6b7a471494..40fb446b16 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -9,7 +9,7 @@ module ApplicationTests boot_rails FileUtils.rm_rf("#{app_path}/config/environments") end - + def test_gems_tasks_are_loaded_first_than_application_ones app_file "lib/tasks/app.rake", <<-RUBY $task_loaded = Rake::Task.task_defined?("db:create:all") diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb new file mode 100644 index 0000000000..d37b7649e2 --- /dev/null +++ b/railties/test/application/runner_test.rb @@ -0,0 +1,49 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + class RunnerTest < Test::Unit::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + + # Lets create a model so we have something to play with + app_file "app/models/user.rb", <<-MODEL + class User + def self.count + 42 + end + end + MODEL + end + + def test_should_run_ruby_statement + assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "puts User.count"` } + end + + def test_should_run_file + app_file "script/count_users.rb", <<-SCRIPT + puts User.count + SCRIPT + + assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "script/count_users.rb"` } + end + + def test_should_set_dollar_0_to_file + app_file "script/dollar0.rb", <<-SCRIPT + puts $0 + SCRIPT + + assert_match "script/dollar0.rb", Dir.chdir(app_path) { `bundle exec rails runner "script/dollar0.rb"` } + end + + def test_should_set_dollar_program_name_to_file + app_file "script/program_name.rb", <<-SCRIPT + puts $PROGRAM_NAME + SCRIPT + + assert_match "script/program_name.rb", Dir.chdir(app_path) { `bundle exec rails runner "script/program_name.rb"` } + end + end +end diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb index 72cae23985..2b6ec26cd0 100644 --- a/railties/test/application/url_generation_test.rb +++ b/railties/test/application/url_generation_test.rb @@ -16,6 +16,7 @@ module ApplicationTests class MyApp < Rails::Application config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" config.session_store :cookie_store, :key => "_myapp_session" + config.active_support.deprecation = :log end MyApp.initialize! diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 65fbf61902..0472ca73a8 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -130,9 +130,9 @@ class ActionsTest < Rails::Generators::TestCase def test_environment_should_include_data_in_environment_initializer_block run_generator - load_paths = 'config.load_paths += %w["#{Rails.root}/app/extras"]' - action :environment, load_paths - assert_file 'config/application.rb', /#{Regexp.escape(load_paths)}/ + autoload_paths = 'config.autoload_paths += %w["#{Rails.root}/app/extras"]' + action :environment, autoload_paths + assert_file 'config/application.rb', /#{Regexp.escape(autoload_paths)}/ end def test_environment_with_block_should_include_block_contents_in_environment_initializer_block diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 6ea722e239..f9d1e42d24 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -62,4 +62,19 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end end + + def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove + migration = "create_books" + run_generator [migration, "title:string", "content:text"] + + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_class_method :up, content do |up| + assert_match /^\s*$/, up + end + + assert_class_method :down, content do |down| + assert_match /^\s*$/, down + end + end + end end diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index e73dd237fb..1badae0713 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -93,6 +93,16 @@ class NamedBaseTest < Rails::Generators::TestCase assert_name g, "application", :application_name end + def test_index_helper + g = generator ['Post'] + assert_name g, 'posts', :index_helper + end + + def test_index_helper_with_uncountable + g = generator ['Sheep'] + assert_name g, 'sheep_index', :index_helper + end + protected def assert_name(generator, value, method) diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb index 96fd7a0a72..55d5bd6f83 100644 --- a/railties/test/generators/resource_generator_test.rb +++ b/railties/test/generators/resource_generator_test.rb @@ -59,14 +59,6 @@ class ResourceGeneratorTest < Rails::Generators::TestCase end end - def test_singleton_resource - run_generator ["account", "--singleton"] - - assert_file "config/routes.rb" do |route| - assert_match /resource :account$/, route - end - end - def test_plural_names_are_singularized content = run_generator ["accounts".freeze] assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index 8040b22fe6..d55ed22975 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -78,20 +78,6 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase end end - def test_generates_singleton_controller - run_generator ["User", "name:string", "age:integer", "--singleton"] - - assert_file "app/controllers/users_controller.rb" do |content| - assert_no_match /def index/, content - end - - assert_file "test/functional/users_controller_test.rb" do |content| - assert_no_match /test "should get index"/, content - end - - assert_no_file "app/views/users/index.html.erb" - end - def test_skip_helper_if_required run_generator ["User", "name:string", "age:integer", "--no-helper"] assert_no_file "app/helpers/users_helper.rb" diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index e8e622fe5c..ea469cb3c8 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -110,4 +110,110 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Stylesheets (should not be removed) assert_file "public/stylesheets/scaffold.css" end + + def test_scaffold_with_namespace_on_invoke + run_generator [ "admin/role", "name:string", "description:string" ] + + # Model + assert_file "app/models/admin.rb", /module Admin/ + assert_file "app/models/admin/role.rb", /class Admin::Role < ActiveRecord::Base/ + assert_file "test/unit/admin/role_test.rb", /class Admin::RoleTest < ActiveSupport::TestCase/ + assert_file "test/fixtures/admin/roles.yml" + assert_migration "db/migrate/create_admin_roles.rb" + + # Route + assert_file "config/routes.rb" do |route| + assert_match /namespace :admin do resources :roles end$/, route + end + + # Controller + assert_file "app/controllers/admin/roles_controller.rb" do |content| + assert_match /class Admin::RolesController < ApplicationController/, content + + assert_instance_method :index, content do |m| + assert_match /@admin_roles = Admin::Role\.all/, m + end + + assert_instance_method :show, content do |m| + assert_match /@admin_role = Admin::Role\.find\(params\[:id\]\)/, m + end + + assert_instance_method :new, content do |m| + assert_match /@admin_role = Admin::Role\.new/, m + end + + assert_instance_method :edit, content do |m| + assert_match /@admin_role = Admin::Role\.find\(params\[:id\]\)/, m + end + + assert_instance_method :create, content do |m| + assert_match /@admin_role = Admin::Role\.new\(params\[:admin_role\]\)/, m + assert_match /@admin_role\.save/, m + assert_match /@admin_role\.errors/, m + end + + assert_instance_method :update, content do |m| + assert_match /@admin_role = Admin::Role\.find\(params\[:id\]\)/, m + assert_match /@admin_role\.update_attributes\(params\[:admin_role\]\)/, m + assert_match /@admin_role\.errors/, m + end + + assert_instance_method :destroy, content do |m| + assert_match /@admin_role = Admin::Role\.find\(params\[:id\]\)/, m + assert_match /@admin_role\.destroy/, m + end + end + + assert_file "test/functional/admin/roles_controller_test.rb", + /class Admin::RolesControllerTest < ActionController::TestCase/ + + # Views + %w( + index + edit + new + show + _form + ).each { |view| assert_file "app/views/admin/roles/#{view}.html.erb" } + assert_no_file "app/views/layouts/admin/roles.html.erb" + + # Helpers + assert_file "app/helpers/admin/roles_helper.rb" + assert_file "test/unit/helpers/admin/roles_helper_test.rb" + + # Stylesheets + assert_file "public/stylesheets/scaffold.css" + end + + def test_scaffold_with_namespace_on_revoke + run_generator [ "admin/role", "name:string", "description:string" ] + run_generator [ "admin/role" ], :behavior => :revoke + + # Model + assert_file "app/models/admin.rb" # ( should not be remove ) + assert_no_file "app/models/admin/role.rb" + assert_no_file "test/unit/admin/role_test.rb" + assert_no_file "test/fixtures/admin/roles.yml" + assert_no_migration "db/migrate/create_admin_roles.rb" + + # Route + assert_file "config/routes.rb" do |route| + assert_no_match /namespace :admin do resources :roles end$/, route + end + + # Controller + assert_no_file "app/controllers/admin/roles_controller.rb" + assert_no_file "test/functional/admin/roles_controller_test.rb" + + # Views + assert_no_file "app/views/admin/roles" + assert_no_file "app/views/layouts/admin/roles.html.erb" + + # Helpers + assert_no_file "app/helpers/admin/roles_helper.rb" + assert_no_file "test/unit/helpers/admin/roles_helper_test.rb" + + # Stylesheets (should not be removed) + assert_file "public/stylesheets/scaffold.css" + end end diff --git a/railties/test/initializable_test.rb b/railties/test/initializable_test.rb index 74301a5dc5..72c35879c5 100644 --- a/railties/test/initializable_test.rb +++ b/railties/test/initializable_test.rb @@ -5,10 +5,7 @@ module InitializableTests class Foo include Rails::Initializable - - class << self - attr_accessor :foo, :bar - end + attr_accessor :foo, :bar initializer :start do @foo ||= 0 @@ -158,30 +155,22 @@ module InitializableTests include ActiveSupport::Testing::Isolation test "initializers run" do - Foo.run_initializers - assert_equal 1, Foo.foo + foo = Foo.new + foo.run_initializers + assert_equal 1, foo.foo end test "initializers are inherited" do - Bar.run_initializers - assert_equal [1, 1], [Bar.foo, Bar.bar] + bar = Bar.new + bar.run_initializers + assert_equal [1, 1], [bar.foo, bar.bar] end test "initializers only get run once" do - Foo.run_initializers - Foo.run_initializers - assert_equal 1, Foo.foo - end - - test "running initializers on children does not effect the parent" do - Bar.run_initializers - assert_nil Foo.foo - assert_nil Foo.bar - end - - test "initializing with modules" do - Word.run_initializers - assert_equal "bird", $word + foo = Foo.new + foo.run_initializers + foo.run_initializers + assert_equal 1, foo.foo end test "creating initializer without a block raises an error" do @@ -198,19 +187,19 @@ module InitializableTests class BeforeAfter < ActiveSupport::TestCase test "running on parent" do $arr = [] - Parent.run_initializers + Parent.new.run_initializers assert_equal [5, 1, 2], $arr end test "running on child" do $arr = [] - Child.run_initializers + Child.new.run_initializers assert_equal [5, 3, 1, 4, 2], $arr end test "handles dependencies introduced before all initializers are loaded" do $arr = [] - Interdependent::Application.run_initializers + Interdependent::Application.new.run_initializers assert_equal [1, 2, 3, 4], $arr end end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index b46ac0efaf..390c0ab543 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -100,7 +100,7 @@ module TestHelpers end end - add_to_config 'config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"; config.session_store :cookie_store, :key => "_myapp_session"' + add_to_config 'config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"; config.session_store :cookie_store, :key => "_myapp_session"; config.active_support.deprecation = :log' end def make_basic_app @@ -110,6 +110,7 @@ module TestHelpers app = Class.new(Rails::Application) app.config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" app.config.session_store :cookie_store, :key => "_myapp_session" + app.config.active_support.deprecation = :log yield app if block_given? app.initialize! diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index 92c7b2ba0e..008247cb1a 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -120,36 +120,36 @@ class PathsTest < ActiveSupport::TestCase test "it is possible to add a path that should be loaded only once" do @root.app = "/app" - @root.app.load_once! - assert @root.app.load_once? - assert @root.load_once.include?(@root.app.paths.first) + @root.app.autoload_once! + assert @root.app.autoload_once? + assert @root.autoload_once.include?(@root.app.paths.first) end test "it is possible to add a path without assignment and specify it should be loaded only once" do - @root.app "/app", :load_once => true - assert @root.app.load_once? - assert @root.load_once.include?("/app") + @root.app "/app", :autoload_once => true + assert @root.app.autoload_once? + assert @root.autoload_once.include?("/app") end test "it is possible to add multiple paths without assignment and specify it should be loaded only once" do - @root.app "/app", "/app2", :load_once => true - assert @root.app.load_once? - assert @root.load_once.include?("/app") - assert @root.load_once.include?("/app2") + @root.app "/app", "/app2", :autoload_once => true + assert @root.app.autoload_once? + assert @root.autoload_once.include?("/app") + assert @root.autoload_once.include?("/app2") end - test "making a path load_once more than once only includes it once in @root.load_once" do + test "making a path autoload_once more than once only includes it once in @root.load_once" do @root.app = "/app" - @root.app.load_once! - @root.app.load_once! - assert_equal 1, @root.load_once.select {|p| p == @root.app.paths.first }.size + @root.app.autoload_once! + @root.app.autoload_once! + assert_equal 1, @root.autoload_once.select {|p| p == @root.app.paths.first }.size end - test "paths added to a load_once path should be added to the load_once collection" do + test "paths added to a load_once path should be added to the autoload_once collection" do @root.app = "/app" - @root.app.load_once! + @root.app.autoload_once! @root.app << "/app2" - assert_equal 2, @root.load_once.size + assert_equal 2, @root.autoload_once.size end test "it is possible to mark a path as eager" do @@ -173,11 +173,11 @@ class PathsTest < ActiveSupport::TestCase end test "it is possible to create a path without assignment and mark it both as eager and load once" do - @root.app "/app", :eager_load => true, :load_once => true + @root.app "/app", :eager_load => true, :autoload_once => true assert @root.app.eager_load? - assert @root.app.load_once? + assert @root.app.autoload_once? assert @root.eager_load.include?("/app") - assert @root.load_once.include?("/app") + assert @root.autoload_once.include?("/app") end test "making a path eager more than once only includes it once in @root.eager_paths" do @@ -218,16 +218,16 @@ class PathsTest < ActiveSupport::TestCase assert_equal ["/app"], @root.load_paths end - test "adding a path to the eager paths also adds it to the load path" do + test "a path can be marked as autoload path" do @root.app = "app" - @root.app.eager_load! - assert_equal ["/foo/bar/app"], @root.load_paths + @root.app.autoload! + @root.app.models = "app/models" + assert_equal ["/foo/bar/app"], @root.autoload_paths end - test "adding a path to the load once paths also adds it to the load path" do - @root.app = "app" - @root.app.load_once! - assert_equal ["/foo/bar/app"], @root.load_paths + test "a path can be marked as autoload on creation" do + @root.app "/app", :autoload => true + assert @root.app.autoload? + assert_equal ["/app"], @root.autoload_paths end - end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index b3f65fd00d..7410a10712 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -20,10 +20,6 @@ module RailtiesTest end end - def reload_config - :reload_engines - end - test "Rails::Engine itself does not respond to config" do boot_rails assert !Rails::Engine.respond_to?(:config) @@ -41,7 +37,7 @@ module RailtiesTest boot_rails - initializers = Rails.application.initializers + initializers = Rails.application.initializers.tsort index = initializers.index { |i| i.name == "dummy_initializer" } selection = initializers[(index-3)..(index)].map(&:name).map(&:to_s) diff --git a/railties/test/railties/i18n_railtie_test.rb b/railties/test/railties/i18n_railtie_test.rb deleted file mode 100644 index 2b1950b3d5..0000000000 --- a/railties/test/railties/i18n_railtie_test.rb +++ /dev/null @@ -1,89 +0,0 @@ -require "isolation/abstract_unit" - -module RailtiesTest - class I18nRailtieTest < Test::Unit::TestCase - include ActiveSupport::Testing::Isolation - - def setup - build_app - boot_rails - FileUtils.rm_rf("#{app_path}/config/environments") - require "rails/all" - end - - def load_app - require "#{app_path}/config/environment" - end - - def assert_fallbacks(fallbacks) - fallbacks.each do |locale, expected| - actual = I18n.fallbacks[locale] - assert_equal expected, actual, "expected fallbacks for #{locale.inspect} to be #{expected.inspect}, but were #{actual.inspect}" - end - end - - def assert_no_fallbacks - assert !I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) - end - - test "config.i18n.load_path gets added to I18n.load_path" do - I18n.load_path = ['existing/path/to/locales'] - I18n::Railtie.config.i18n.load_path = ['new/path/to/locales'] - load_app - - assert I18n.load_path.include?('existing/path/to/locales') - assert I18n.load_path.include?('new/path/to/locales') - end - - test "not using config.i18n.fallbacks does not initialize I18n.fallbacks" do - I18n.backend = Class.new { include I18n::Backend::Base }.new - load_app - assert_no_fallbacks - end - - test "config.i18n.fallbacks = true initializes I18n.fallbacks with default settings" do - I18n::Railtie.config.i18n.fallbacks = true - load_app - assert I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) - assert_fallbacks :de => [:de, :en] - end - - test "config.i18n.fallbacks = true initializes I18n.fallbacks with default settings even when backend changes" do - I18n::Railtie.config.i18n.fallbacks = true - I18n::Railtie.config.i18n.backend = Class.new { include I18n::Backend::Base }.new - load_app - assert I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) - assert_fallbacks :de => [:de, :en] - end - - test "config.i18n.fallbacks.defaults = [:'en-US'] initializes fallbacks with en-US as a fallback default" do - I18n::Railtie.config.i18n.fallbacks.defaults = [:'en-US'] - load_app - assert_fallbacks :de => [:de, :'en-US', :en] - end - - test "config.i18n.fallbacks.map = { :ca => :'es-ES' } initializes fallbacks with a mapping ca => es-ES" do - I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' } - load_app - assert_fallbacks :ca => [:ca, :"es-ES", :es, :en] - end - - test "[shortcut] config.i18n.fallbacks = [:'en-US'] initializes fallbacks with en-US as a fallback default" do - I18n::Railtie.config.i18n.fallbacks = [:'en-US'] - load_app - assert_fallbacks :de => [:de, :'en-US', :en] - end - - test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping de-AT => de-DE" do - I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' } - load_app - assert_fallbacks :ca => [:ca, :"es-ES", :es, :en] - end - - test "[shortcut] config.i18n.fallbacks = [:'en-US', { :ca => :'es-ES' }] initializes fallbacks with the given arguments" do - I18n::Railtie.config.i18n.fallbacks = [:'en-US', { :ca => :'es-ES' }] - load_app - assert_fallbacks :ca => [:ca, :"es-ES", :es, :'en-US', :en] - end - end -end
\ No newline at end of file diff --git a/railties/test/railties/plugin_test.rb b/railties/test/railties/plugin_test.rb index 0f5f29468c..b143f56484 100644 --- a/railties/test/railties/plugin_test.rb +++ b/railties/test/railties/plugin_test.rb @@ -15,10 +15,6 @@ module RailtiesTest end end - def reload_config - :reload_plugins - end - test "Rails::Plugin itself does not respond to config" do boot_rails assert !Rails::Plugin.respond_to?(:config) @@ -37,6 +33,32 @@ module RailtiesTest assert_equal "Bukkits", Bukkits.name end + test "plugin gets added to dependency list" do + boot_rails + assert_equal "Another", Another.name + end + + test "plugin constants get reloaded if config.reload_plugins is set to true" do + add_to_config <<-RUBY + config.reload_plugins = true + RUBY + + boot_rails + + assert_equal "Another", Another.name + ActiveSupport::Dependencies.clear + @plugin.delete("lib/another.rb") + assert_raises(NameError) { Another } + end + + test "plugin constants are not reloaded by default" do + boot_rails + assert_equal "Another", Another.name + ActiveSupport::Dependencies.clear + @plugin.delete("lib/another.rb") + assert_nothing_raised { Another } + end + test "it loads the plugin's init.rb file" do boot_rails assert_equal "loaded", BUKKITS diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb index 2accaca855..c74cc01dc1 100644 --- a/railties/test/railties/railtie_test.rb +++ b/railties/test/railties/railtie_test.rb @@ -48,15 +48,6 @@ module RailtiesTest assert_equal "hello", AppTemplate::Application.config.foo.greetings end - test "railtie can add log subscribers" do - begin - class Foo < Rails::Railtie ; log_subscriber(:foo, Rails::LogSubscriber.new) ; end - assert_kind_of Rails::LogSubscriber, Rails::LogSubscriber.log_subscribers[0] - ensure - Rails::LogSubscriber.log_subscribers.clear - end - end - test "railtie can add to_prepare callbacks" do $to_prepare = false class Foo < Rails::Railtie ; config.to_prepare { $to_prepare = true } ; end diff --git a/railties/test/railties/shared_tests.rb b/railties/test/railties/shared_tests.rb index 3f78d7d3fe..ce7c55c11c 100644 --- a/railties/test/railties/shared_tests.rb +++ b/railties/test/railties/shared_tests.rb @@ -10,51 +10,25 @@ module RailtiesTest @app ||= Rails.application end - def test_plugin_puts_its_lib_directory_on_load_path + def test_puts_its_lib_directory_on_load_path boot_rails require "another" assert_equal "Another", Another.name end - def test_plugin_paths_get_added_to_as_dependency_list - boot_rails - assert_equal "Another", Another.name - end - - def test_plugins_constants_are_not_reloaded_by_default - boot_rails - assert_equal "Another", Another.name - ActiveSupport::Dependencies.clear - @plugin.delete("lib/another.rb") - assert_nothing_raised { Another } - end - - def test_plugin_constants_get_reloaded_if_config_reload_plugins - add_to_config <<-RUBY - config.#{reload_config} = true - RUBY - - boot_rails - - assert_equal "Another", Another.name - ActiveSupport::Dependencies.clear - @plugin.delete("lib/another.rb") - assert_raises(NameError) { Another } - end - - def test_plugin_puts_its_models_directory_on_load_path + def test_puts_its_models_directory_on_autoload_path @plugin.write "app/models/my_bukkit.rb", "class MyBukkit ; end" boot_rails assert_nothing_raised { MyBukkit } end - def test_plugin_puts_its_controllers_directory_on_the_load_path + def test_puts_its_controllers_directory_on_autoload_path @plugin.write "app/controllers/bukkit_controller.rb", "class BukkitController ; end" boot_rails assert_nothing_raised { BukkitController } end - def test_plugin_adds_its_views_to_view_paths + def test_adds_its_views_to_view_paths @plugin.write "app/controllers/bukkit_controller.rb", <<-RUBY class BukkitController < ActionController::Base def index @@ -72,7 +46,7 @@ module RailtiesTest assert_equal "Hello bukkits\n", response[2].body end - def test_plugin_adds_its_views_to_view_paths_with_lower_proriority + def test_adds_its_views_to_view_paths_with_lower_proriority_than_app_ones @plugin.write "app/controllers/bukkit_controller.rb", <<-RUBY class BukkitController < ActionController::Base def index @@ -91,7 +65,7 @@ module RailtiesTest assert_equal "Hi bukkits\n", response[2].body end - def test_plugin_adds_helpers_to_controller_views + def test_adds_helpers_to_controller_views @plugin.write "app/controllers/bukkit_controller.rb", <<-RUBY class BukkitController < ActionController::Base def index @@ -116,11 +90,10 @@ module RailtiesTest assert_equal "Hello bukkits\n", response[2].body end - def test_plugin_eager_load_any_path_under_app + def test_autoload_any_path_under_app @plugin.write "app/anything/foo.rb", <<-RUBY module Foo; end RUBY - boot_rails assert Foo end @@ -269,7 +242,7 @@ YAML assert_equal "Rendered from namespace", last_response.body end - def test_plugin_initializers + def test_initializers $plugin_initializer = false @plugin.write "config/initializers/foo.rb", <<-RUBY $plugin_initializer = true @@ -279,7 +252,7 @@ YAML assert $plugin_initializer end - def test_plugin_midleware_referenced_in_configuration + def test_midleware_referenced_in_configuration @plugin.write "lib/bukkits.rb", <<-RUBY class Bukkits def initialize(app) diff --git a/tools/profile b/tools/profile index f02f1b5057..a6e3b41900 100755 --- a/tools/profile +++ b/tools/profile @@ -39,6 +39,7 @@ end GC.start before = GC.allocated_size +before_gctime, before_gcruns = GC.time, GC.collections before_rss = `ps -o rss= -p #{Process.pid}`.to_i before_live_objects = ObjectSpace.live_objects @@ -54,6 +55,7 @@ end elapsed = Benchmark.realtime { require path } results = RubyProf.stop if mode +after_gctime, after_gcruns = GC.time, GC.collections GC.start after_live_objects = ObjectSpace.live_objects after_rss = `ps -o rss= -p #{Process.pid}`.to_i @@ -61,7 +63,9 @@ after = GC.allocated_size usage = (after - before) / 1024.0 if mode - if RubyProf.const_defined?(:CallStackPrinter) + if printer = ARGV.shift + RubyProf.const_get("#{printer.to_s.classify}Printer").new(results).print($stdout) + elsif RubyProf.const_defined?(:CallStackPrinter) File.open("#{File.basename(path, '.rb')}.#{mode}.html", 'w') do |out| RubyProf::CallStackPrinter.new(results).print(out) end @@ -79,4 +83,4 @@ RequireProfiler.stats.each do |file, depth, sec, bytes, objects| puts "#{' ' * (42 + depth)}#{file}" end end -puts "%10.2f KB %10d obj %8.1f ms %d KB RSS" % [usage, after_live_objects - before_live_objects, elapsed * 1000, after_rss - before_rss] +puts "%10.2f KB %10d obj %8.1f ms %d KB RSS %8.1f ms GC time %d GC runs" % [usage, after_live_objects - before_live_objects, elapsed * 1000, after_rss - before_rss, (after_gctime - before_gctime) / 1000.0, after_gcruns - before_gcruns] diff --git a/version.rb b/version.rb index c10876134a..34d96fd0f3 100644 --- a/version.rb +++ b/version.rb @@ -3,7 +3,7 @@ module Rails MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta3" + BUILD = "beta4" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end |