diff options
358 files changed, 8214 insertions, 4106 deletions
diff --git a/.gitignore b/.gitignore index 2dfdf96e7f..a18fba3780 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ activesupport/doc activemodel/test/fixtures/fixture_database.sqlite3 actionpack/test/tmp activesupport/test/fixtures/isolation_test +dist railties/test/500.html railties/test/fixtures/tmp railties/test/initializer/root/log @@ -32,13 +32,14 @@ platforms :mri_18 do end platforms :mri_19 do - gem "ruby-debug19" + # TODO: Remove the conditional when ruby-debug19 supports Ruby >= 1.9.3 + gem "ruby-debug19" if RUBY_VERSION < "1.9.3" end platforms :ruby do gem 'json' gem 'yajl-ruby' - gem "nokogiri", ">= 1.4.3.1" + gem "nokogiri", ">= 1.4.4" # AR gem "sqlite3-ruby", "~> 1.3.1", :require => 'sqlite3' diff --git a/Rakefile b/Rakefile index 5065fb286b..1f3c770c77 100644..100755 --- a/Rakefile +++ b/Rakefile @@ -1,9 +1,18 @@ +#!/usr/bin/env rake gem 'rdoc', '>= 2.5.10' require 'rdoc' -require 'rake' require 'rdoc/task' -require 'rake/gempackagetask' +require 'net/http' + +$:.unshift File.expand_path('..', __FILE__) +require "tasks/release" + +desc "Build gem files for all projects" +task :build => "all:build" + +desc "Release all gems to gemcutter and create a tag" +task :release => "all:release" # RDoc skips some files in the Rails tree due to its binary? predicate. This is a quick # hack for edge docs, until we decide which is the correct way to address this issue. @@ -54,27 +63,6 @@ task :smoke do system %(cd activerecord && #{$0} sqlite3:isolated_test) end -spec = eval(File.read('rails.gemspec')) -Rake::GemPackageTask.new(spec) do |pkg| - pkg.gem_spec = spec -end - -desc "Release all gems to gemcutter. Package rails, package & push components, then push rails" -task :release => :release_projects do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end - -desc "Release all components to gemcutter." -task :release_projects => :package do - errors = [] - PROJECTS.each do |project| - system(%(cd #{project} && #{$0} release)) || errors << project - end - fail("Errors in #{errors.join(', ')}") unless errors.empty? -end - desc "Install gems for all projects." task :install => :gem do version = File.read("RAILS_VERSION").strip @@ -172,3 +160,27 @@ task :update_versions do end end end + +# +# We have a webhook configured in Github that gets invoked after pushes. +# This hook triggers the following tasks: +# +# * updates the local checkout +# * updates Rails Contributors +# * generates and publishes edge docs +# * if there's a new stable tag, generates and publishes stable docs +# +# Everything is automated and you do NOT need to run this task normally. +# +# We publish a new version by tagging, and pushing a tag does not trigger +# that webhook. Stable docs would be updated by any subsequent regular +# push, but if you want that to happen right away just run this. +# +desc 'Publishes docs, run this AFTER a new stable tag has been pushed' +task :publish_docs do + Net::HTTP.new('rails-hooks.hashref.com').start do |http| + request = Net::HTTP::Post.new('/rails-master-hook') + response = http.request(request) + puts response.body + end +end diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG index 9c073dce1f..167c1da9c5 100644 --- a/actionmailer/CHANGELOG +++ b/actionmailer/CHANGELOG @@ -2,6 +2,10 @@ * No changes +*Rails 3.0.2 (unreleased)* + +* No changes + *Rails 3.0.1 (October 15, 2010)* * No Changes, just a version bump. diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile index cba6e93c8a..123ef9bbbf 100644..100755 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rake/gempackagetask' diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 042ad43319..29b5813785 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -20,5 +20,5 @@ Gem::Specification.new do |s| s.has_rdoc = true s.add_dependency('actionpack', version) - s.add_dependency('mail', '~> 2.2.6.1') + s.add_dependency('mail', '~> 2.2.9') end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 0bf60a2c24..00226148ed 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -234,8 +234,8 @@ module ActionMailer #:nodoc: # default :sender => 'system@example.com' # end # - # You can pass in any header value that a <tt>Mail::Message</tt>, out of the box, <tt>ActionMailer::Base</tt> - # sets the following: + # You can pass in any header value that a <tt>Mail::Message</tt> accepts. Out of the box, + # <tt>ActionMailer::Base</tt> sets the following: # # * <tt>:mime_version => "1.0"</tt> # * <tt>:charset => "UTF-8",</tt> @@ -273,7 +273,7 @@ module ActionMailer #:nodoc: # = Configuration options # # These options are specified on the class level, like - # <tt>ActionMailer::Base.template_root = "/my/templates"</tt> + # <tt>ActionMailer::Base.raise_delivery_errors = true</tt> # # * <tt>default</tt> - You can pass this in at a class level as well as within the class itself as # per the above section. diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index f4d1bb59f1..63e18147f6 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/class/attribute' + module ActionMailer class NonInferrableMailerError < ::StandardError def initialize(name) @@ -15,11 +17,11 @@ module ActionMailer module ClassMethods def tests(mailer) - write_inheritable_attribute(:mailer_class, mailer) + self._mailer_class = mailer end def mailer_class - if mailer = read_inheritable_attribute(:mailer_class) + if mailer = self._mailer_class mailer else tests determine_default_mailer(name) @@ -65,6 +67,7 @@ module ActionMailer end included do + class_attribute :_mailer_class setup :initialize_test_deliveries setup :set_expected_mail end diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index 7dd3d401b1..27eb6c2b9b 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -3,8 +3,8 @@ module ActionMailer MAJOR = 3 MINOR = 1 TINY = 0 - BUILD = "beta" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 2d133ca0b2..74d017cc3d 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.1.0 (unreleased)* +* Added config.action_controller.include_all_helpers. By default 'helper :all' is done in ActionController::Base, which includes all the helpers by default. Setting include_all_helpers to false will result in including only application_helper and helper corresponding to controller (like foo_helper for foo_controller). [Piotr Sarnacki] + * Added a convenience idiom to generate HTML5 data-* attributes in tag helpers from a :data hash of options: tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)}) @@ -23,6 +25,10 @@ * Add Rack::Cache to the default stack. Create a Rails store that delegates to the Rails cache, so by default, whatever caching layer you are using will be used for HTTP caching. Note that Rack::Cache will be used if you use #expires_in, #fresh_when or #stale with :public => true. Otherwise, the caching rules will apply to the browser only. [Yehuda Katz, Carl Lerche] +*Rails 3.0.2 (unreleased)* + +* The helper number_to_currency accepts a new :negative_format option to be able to configure how to render negative amounts. [Don Wilson] + *Rails 3.0.1 (October 15, 2010)* * No Changes, just a version bump. diff --git a/actionpack/Rakefile b/actionpack/Rakefile index a6ce08113f..9030db9f7a 100644..100755 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rake/gempackagetask' diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 40eafd1a2f..44967631ff 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -22,9 +22,8 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) s.add_dependency('rack-cache', '~> 0.5.3') - s.add_dependency('rack-cache-purge', '~> 0.0.1') - s.add_dependency('builder', '~> 2.1.2') - s.add_dependency('i18n', '~> 0.4.1') + s.add_dependency('builder', '~> 3.0.0') + s.add_dependency('i18n', '~> 0.4.2') s.add_dependency('rack', '~> 1.2.1') s.add_dependency('rack-test', '~> 0.5.6') s.add_dependency('rack-mount', '~> 0.6.13') diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 7b0d80614d..f169ab7c3a 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -81,27 +81,29 @@ module AbstractController class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 # Append a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def #{filter}_filter(*names, &blk) # def before_filter(*names, &blk) - _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options} - set_callback(:process_action, :#{filter}, name, options) # set_callback(:process_action, :before_filter, name, options) - end # end - end # end + def #{filter}_filter(*names, &blk) + _insert_callbacks(names, blk) do |name, options| + options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} + set_callback(:process_action, :#{filter}, name, options) + end + end # Prepend a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def prepend_#{filter}_filter(*names, &blk) # def prepend_before_filter(*names, &blk) - _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| - set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true)) # set_callback(:process_action, :before, name, options.merge(:prepend => true)) - end # end - end # end + def prepend_#{filter}_filter(*names, &blk) + _insert_callbacks(names, blk) do |name, options| + options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} + set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true)) + end + end # Skip a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def skip_#{filter}_filter(*names, &blk) # def skip_before_filter(*names, &blk) - _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| - skip_callback(:process_action, :#{filter}, name, options) # skip_callback(:process_action, :before, name, options) - end # end - end # end + def skip_#{filter}_filter(*names, &blk) + _insert_callbacks(names, blk) do |name, options| + skip_callback(:process_action, :#{filter}, name, options) + end + end # *_filter is the same as append_*_filter alias_method :append_#{filter}_filter, :#{filter}_filter diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 1c63fb2d14..91b75273fa 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -46,14 +46,14 @@ module AbstractController @view_context_class ||= begin controller = self Class.new(ActionView::Base) do + if controller.respond_to?(:_routes) && controller._routes + include controller._routes.url_helpers + include controller._routes.mounted_helpers + end + if controller.respond_to?(:_helpers) include controller._helpers - if controller.respond_to?(:_routes) && controller._routes - include controller._routes.url_helpers - include controller._routes.mounted_helpers - end - # TODO: Fix RJS to not require this self.helpers = controller._helpers end @@ -119,7 +119,7 @@ module AbstractController controller_path end - private + private # This method should return a hash with assigns. # You can overwrite this configuration per controller. @@ -128,7 +128,7 @@ module AbstractController hash = {} variables = instance_variable_names variables -= protected_instance_variables if respond_to?(:protected_instance_variables) - variables.each { |name| hash[name.to_s[1..-1]] = instance_variable_get(name) } + variables.each { |name| hash[name.to_s[1, name.length]] = instance_variable_get(name) } hash end @@ -138,13 +138,13 @@ module AbstractController case action when NilClass when Hash - options, action = action, nil + options = action when String, Symbol action = action.to_s key = action.include?(?/) ? :file : :action options[key] = action else - options.merge!(:partial => action) + options[:partial] = action end options diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index d14831b763..91a88ab68a 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -53,8 +53,9 @@ module ActionController include AbstractController::Helpers included do - config_accessor :helpers_path + config_accessor :helpers_path, :include_all_helpers self.helpers_path ||= [] + self.include_all_helpers = true end module ClassMethods diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index c6d4c6d936..9ba37134b8 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -227,7 +227,7 @@ module ActionController #:nodoc: "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? if response = retrieve_response_from_mimes(&block) - options = resources.extract_options! + options = resources.size == 1 ? {} : resources.extract_options! options.merge!(:default_response => response) (options.delete(:responder) || self.class.responder).call(self, resources, options) end @@ -258,9 +258,8 @@ module ActionController #:nodoc: # nil if :not_acceptable was sent to the client. # def retrieve_response_from_mimes(mimes=nil, &block) - collector = Collector.new { default_render } mimes ||= collect_mimes_from_class_level - mimes.each { |mime| collector.send(mime) } + collector = Collector.new(mimes) { default_render } block.call(collector) if block_given? if format = request.negotiate_mime(collector.order) @@ -277,8 +276,9 @@ module ActionController #:nodoc: include AbstractController::Collector attr_accessor :order - def initialize(&block) + def initialize(mimes, &block) @order, @responses, @default_response = [], {}, block + mimes.each { |mime| send(mime) } end def any(*args, &block) @@ -291,7 +291,7 @@ module ActionController #:nodoc: alias :all :any def custom(mime_type, &block) - mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s) + mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type) @order << mime_type @responses[mime_type] ||= block end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index f9b226b7c9..d6f6ab1855 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -15,30 +15,12 @@ module ActionController end module ClassMethods - def _write_render_options - renderers = _renderers.map do |name, value| - <<-RUBY_EVAL - if options.key?(:#{name}) - _process_options(options) - return _render_option_#{name}(options.delete(:#{name}), options) - end - RUBY_EVAL - end - - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def _handle_render_options(options) - #{renderers.join} - end - RUBY_EVAL - end - def use_renderers(*args) new = _renderers.dup args.each do |key| new[key] = RENDERERS[key] end self._renderers = new.freeze - _write_render_options end alias use_renderer use_renderers end @@ -47,31 +29,33 @@ module ActionController _handle_render_options(options) || super end + def _handle_render_options(options) + _renderers.each do |name, value| + if options.key?(name.to_sym) + _process_options(options) + return send("_render_option_#{name}", options.delete(name.to_sym), options) + end + end + nil + end + RENDERERS = {} def self.add(key, &block) define_method("_render_option_#{key}", &block) RENDERERS[key] = block - All._write_render_options end module All extend ActiveSupport::Concern include Renderers - INCLUDED = [] included do self._renderers = RENDERERS - _write_render_options - INCLUDED << self - end - - def self._write_render_options - INCLUDED.each(&:_write_render_options) end end add :json do |json, options| - json = json.to_json(options) unless json.respond_to?(:to_str) + json = json.to_json(options) unless json.kind_of?(String) json = "#{options[:callback]}(#{json})" unless options[:callback].blank? self.content_type ||= Mime::JSON self.response_body = json diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index e524e546ad..14cc547dd0 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -20,36 +20,35 @@ module ActionController private - # Normalize arguments by catching blocks and setting them on :update. - def _normalize_args(action=nil, options={}, &blk) #:nodoc: - options = super - options[:update] = blk if block_given? - options - end - - # Normalize both text and status options. - def _normalize_options(options) #:nodoc: - if options.key?(:text) && options[:text].respond_to?(:to_text) - options[:text] = options[:text].to_text - end + # Normalize arguments by catching blocks and setting them on :update. + def _normalize_args(action=nil, options={}, &blk) #:nodoc: + options = super + options[:update] = blk if block_given? + options + end - if options[:status] - options[:status] = Rack::Utils.status_code(options[:status]) - end + # Normalize both text and status options. + def _normalize_options(options) #:nodoc: + if options.key?(:text) && options[:text].respond_to?(:to_text) + options[:text] = options[:text].to_text + end - super + if options[:status] + options[:status] = Rack::Utils.status_code(options[:status]) end - # Process controller specific options, as status, content-type and location. - def _process_options(options) #:nodoc: - status, content_type, location = options.values_at(:status, :content_type, :location) + super + end - self.status = status if status - self.content_type = content_type if content_type - self.headers["Location"] = url_for(location) if location + # Process controller specific options, as status, content-type and location. + def _process_options(options) #:nodoc: + status, content_type, location = options.values_at(:status, :content_type, :location) - super - end + self.status = status if status + self.content_type = content_type if content_type + self.headers["Location"] = url_for(location) if location + super + end end end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 02f577647e..148efbb081 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -53,9 +53,13 @@ module ActionController #:nodoc: # class FooController < ApplicationController # protect_from_forgery :except => :index # - # # you can disable csrf protection on controller-by-controller basis: - # skip_before_filter :verify_authenticity_token - # end + # You can disable csrf protection on controller-by-controller basis: + # + # skip_before_filter :verify_authenticity_token + # + # It can also be disabled for specific controller actions: + # + # skip_before_filter :verify_authenticity_token, :except => [:create] # # Valid Options: # diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index 4b8c452d50..f4efeb33ba 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -14,18 +14,9 @@ module ActionController cookies.write(@_response) end @_response.prepare! - set_test_assigns ret end - def set_test_assigns - @assigns = {} - (instance_variable_names - self.class.protected_instance_variables).each do |var| - name, value = var[1..-1], instance_variable_get(var) - @assigns[name] = value - end - end - # TODO : Rewrite tests using controller.headers= to use Rack env def headers=(new_headers) @_response ||= ActionDispatch::Response.new diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 333eeaeffb..6fc0cf1fb8 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -6,7 +6,8 @@ module ActionController def url_options @_url_options ||= super.reverse_merge( - :host => request.host_with_port, + :host => request.host, + :port => request.optional_port, :protocol => request.protocol, :_path_segments => request.symbolized_path_parameters ).freeze @@ -20,5 +21,6 @@ module ActionController @_url_options end end + end end diff --git a/actionpack/lib/action_controller/railties/paths.rb b/actionpack/lib/action_controller/railties/paths.rb index 7a59d4f2f3..699c44c62c 100644 --- a/actionpack/lib/action_controller/railties/paths.rb +++ b/actionpack/lib/action_controller/railties/paths.rb @@ -13,7 +13,9 @@ module ActionController end klass.helpers_path = paths - klass.helper :all if klass.superclass == ActionController::Base + if klass.superclass == ActionController::Base && ActionController::Base.include_all_helpers + klass.helper :all + end end end end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 0c26071379..0f43527a56 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,6 +1,7 @@ require 'rack/session/abstract/id' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' +require 'active_support/core_ext/class/attribute' module ActionController module TemplateAssertions @@ -325,11 +326,11 @@ module ActionController def controller_class=(new_class) prepare_controller_class(new_class) if new_class - write_inheritable_attribute(:controller_class, new_class) + self._controller_class = new_class end def controller_class - if current_controller_class = read_inheritable_attribute(:controller_class) + if current_controller_class = self._controller_class current_controller_class else self.controller_class = determine_default_controller_class(name) @@ -411,8 +412,9 @@ module ActionController @controller.request = @request @controller.params.merge!(parameters) build_request_uri(action, parameters) - Base.class_eval { include Testing } + @controller.class.class_eval { include Testing } @controller.process_with_new_base_test(@request, @response) + @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {} @request.session.delete('flash') if @request.session['flash'].blank? @response end @@ -441,6 +443,7 @@ module ActionController included do include ActionController::TemplateAssertions include ActionDispatch::Assertions + class_attribute :_controller_class setup :setup_controller_request_and_response end @@ -448,7 +451,7 @@ module ActionController def build_request_uri(action, parameters) unless @request.env["PATH_INFO"] - options = @controller.__send__(:url_options).merge(parameters) + options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters options.update( :only_path => true, :action => action, diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb index dceddb9b80..3e5d23b5c1 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb @@ -1,5 +1,5 @@ require 'set' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' module HTML class Sanitizer @@ -60,7 +60,7 @@ module HTML class WhiteListSanitizer < Sanitizer [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags, :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr| - class_inheritable_accessor attr, :instance_writer => false + class_attribute attr, :instance_writer => false end # A regular expression of the valid characters used to separate protocols like diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index b959aa258e..68ba1a81b5 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -36,19 +36,21 @@ module ActionDispatch # # GET /posts/5.xml | request.format => Mime::XML # GET /posts/5.xhtml | request.format => Mime::HTML - # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt> + # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first # def format(view_path = []) formats.first end + BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/ + def formats accept = @env['HTTP_ACCEPT'] @env["action_dispatch.request.formats"] ||= if parameters[:format] Array(Mime[parameters[:format]]) - elsif xhr? || (accept && accept !~ /,\s*\*\/\*/) + elsif xhr? || (accept && accept !~ BROWSER_LIKE_ACCEPTS) accepts else [Mime::HTML] diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 8f1c9b6691..08eab5634a 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -80,6 +80,9 @@ module Mime end class << self + + TRAILING_STAR_REGEXP = /(text|application)\/\*/ + def lookup(string) LOOKUP[string] end @@ -105,7 +108,11 @@ module Mime def parse(accept_header) if accept_header !~ /,/ - [Mime::Type.lookup(accept_header)] + if accept_header =~ TRAILING_STAR_REGEXP + parse_data_with_trailing_star($1) + else + [Mime::Type.lookup(accept_header)] + end else # keep track of creation order to keep the subsequent sort stable list = [] @@ -113,7 +120,11 @@ module Mime params, q = header.split(/;\s*q=/) if params params.strip! - list << AcceptItem.new(index, params, q) unless params.empty? + if params =~ TRAILING_STAR_REGEXP + parse_data_with_trailing_star($1).each { |m| list << AcceptItem.new(index, m.to_s, q) } + else + list << AcceptItem.new(index, params, q) unless params.empty? + end end end list.sort! @@ -160,6 +171,28 @@ module Mime list end end + + # input: 'text' + # returend value: [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT] + # + # input: 'application' + # returend value: [Mime::HTML, Mime::JS, Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM + def parse_data_with_trailing_star(input) + keys = Mime::LOOKUP.keys.select{|k| k.include?(input)} + Mime::LOOKUP.values_at(*keys).uniq + end + + # This method is opposite of register method. + # + # Usage: + # + # Mime::Type.unregister("text/x-mobile", :mobile) + def unregister(string, symbol) + EXTENSION_LOOKUP.delete(symbol.to_s) + LOOKUP.delete(string) + symbol = symbol.to_s.upcase.intern + Mime.module_eval { remove_const(symbol) if const_defined?(symbol) } + end end def initialize(string, symbol = nil, synonyms = []) diff --git a/actionpack/lib/action_dispatch/http/rack_cache.rb b/actionpack/lib/action_dispatch/http/rack_cache.rb index e5914abc81..b5c1435903 100644 --- a/actionpack/lib/action_dispatch/http/rack_cache.rb +++ b/actionpack/lib/action_dispatch/http/rack_cache.rb @@ -21,11 +21,6 @@ module ActionDispatch @store.write(key, value) end - def purge(key) - @store.delete(key) - nil - end - ::Rack::Cache::MetaStore::RAILS = self end @@ -58,10 +53,6 @@ module ActionDispatch [key, size] end - def purge(key) - @store.delete(key) - end - ::Rack::Cache::EntityStore::RAILS = self end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 55a3166f6d..08f30e068d 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -4,6 +4,7 @@ require 'strscan' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/string/access' +require 'active_support/inflector' require 'action_dispatch/http/headers' module ActionDispatch @@ -44,8 +45,24 @@ module ActionDispatch @env.key?(key) end - HTTP_METHODS = %w(get head put post delete options) - HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h } + # List of HTTP request methods from the following RFCs: + # Hypertext Transfer Protocol -- HTTP/1.1 (http://www.ietf.org/rfc/rfc2616.txt) + # HTTP Extensions for Distributed Authoring -- WEBDAV (http://www.ietf.org/rfc/rfc2518.txt) + # Versioning Extensions to WebDAV (http://www.ietf.org/rfc/rfc3253.txt) + # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt) + # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt) + # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt) + # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt) + RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) + RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) + RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY) + RFC3648 = %w(ORDERPATCH) + RFC3744 = %w(ACL) + RFC5323 = %w(SEARCH) + RFC5789 = %w(PATCH) + + HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789 + HTTP_METHOD_LOOKUP = Hash.new { |h, m| h[m] = m.underscore.to_sym if HTTP_METHODS.include?(m) } # Returns the HTTP \method that the application should see. # In the case where the \method was overridden by a middleware diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 84e58d7d6a..37effade4f 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/blank' - module ActionDispatch module Http class UploadedFile @@ -13,6 +11,14 @@ module ActionDispatch raise(ArgumentError, ':tempfile is required') unless @tempfile end + def open + @tempfile.open + end + + def path + @tempfile.path + end + def read(*args) @tempfile.read(*args) end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index cfee95eb4b..1e7054f381 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -2,17 +2,34 @@ module ActionDispatch module Http module URL mattr_accessor :tld_length + self.tld_length = 1 + + def self.extract_domain(host, tld_length = @@tld_length) + return nil unless named_host?(host) + + host.split('.').last(1 + tld_length).join('.') + end + + def self.extract_subdomains(host, tld_length = @@tld_length) + return [] unless named_host?(host) + parts = host.split('.') + parts[0..-(tld_length+2)] + end + + def self.extract_subdomain(host, tld_length = @@tld_length) + extract_subdomains(host, tld_length).join('.') + end + + def self.named_host?(host) + !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) + end + # Returns the complete URL used for this request. def url protocol + host_with_port + fullpath end - # Returns 'https' if this is an SSL request and 'http' otherwise. - def scheme - ssl? ? 'https' : 'http' - end - # Returns 'https://' if this is an SSL request and 'http://' otherwise. def protocol @protocol ||= ssl? ? 'https://' : 'http://' @@ -40,10 +57,12 @@ module ActionDispatch # Returns the port number of this request as an integer. def port - if raw_host_with_port =~ /:(\d+)$/ - $1.to_i - else - standard_port + @port ||= begin + if raw_host_with_port =~ /:(\d+)$/ + $1.to_i + else + standard_port + end end end @@ -60,10 +79,16 @@ module ActionDispatch port == standard_port end - # Returns a \port suffix like ":8080" if the \port number of this request + # Returns a number \port suffix like 8080 if the \port number of this request # is not the default HTTP \port 80 or HTTPS \port 443. + def optional_port + standard_port? ? nil : port + end + + # Returns a string \port suffix, including colon, like ":8080" if the \port + # number of this request is not the default HTTP \port 80 or HTTPS \port 443. def port_string - port == standard_port ? '' : ":#{port}" + standard_port? ? '' : ":#{port}" end def server_port @@ -72,10 +97,8 @@ module ActionDispatch # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". - def domain(tld_length = 1) - return nil unless named_host?(host) - - host.split('.').last(1 + tld_length).join('.') + def domain(tld_length = @@tld_length) + ActionDispatch::Http::URL.extract_domain(host, tld_length) end # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be @@ -83,20 +106,17 @@ module ActionDispatch # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt> # in "www.rubyonrails.co.uk". def subdomains(tld_length = @@tld_length) - return [] unless named_host?(host) - parts = host.split('.') - parts[0..-(tld_length+2)] + ActionDispatch::Http::URL.extract_subdomains(host, tld_length) end + # Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be + # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, + # such as 2 to catch <tt>["www"]</tt> instead of <tt>"www.rubyonrails"</tt> + # in "www.rubyonrails.co.uk". def subdomain(tld_length = @@tld_length) - subdomains(tld_length).join('.') + subdomains(tld_length) end - private - - def named_host?(host) - !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) - end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 75c8cc3dd0..b0a4e3d949 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -16,17 +16,23 @@ module ActionDispatch # Examples for writing: # # # Sets a simple session cookie. + # # This cookie will be deleted when the user's browser is closed. # cookies[:user_name] = "david" # + # # Assign an array of values to a cookie. + # cookies[:lat_lon] = [47.68, -122.37] + # # # Sets a cookie that expires in 1 hour. # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } # # # Sets a signed cookie, which prevents a user from tampering with its value. - # # You must specify a value in ActionController::Base.cookie_verifier_secret. - # cookies.signed[:remember_me] = [current_user.id, current_user.salt] + # # The cookie is signed by your app's <tt>config.secret_token</tt> value. + # # Rails generates this value by default when you create a new Rails app. + # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). # cookies.permanent[:login] = "XJ-122" + # # # You can also chain these methods: # cookies.permanent.signed[:login] = "XJ-122" # @@ -34,6 +40,7 @@ module ActionDispatch # # cookies[:user_name] # => "david" # cookies.size # => 2 + # cookies[:lat_lon] # => [47.68, -122.37] # # Example for deleting: # @@ -98,17 +105,19 @@ module ActionDispatch def self.build(request) secret = request.env[TOKEN_KEY] host = request.host + secure = request.ssl? - new(secret, host).tap do |hash| + new(secret, host, secure).tap do |hash| hash.update(request.cookies) end end - def initialize(secret = nil, host = nil) + def initialize(secret = nil, host = nil, secure = false) @secret = secret @set_cookies = {} @delete_cookies = {} @host = host + @secure = secure super() end @@ -193,9 +202,15 @@ module ActionDispatch end def write(headers) - @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) } + @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } end + + private + + def write_cookie?(cookie) + @secure || !cookie[:secure] || defined?(Rails.env) && Rails.env.development? + end end class PermanentCookieJar < CookieJar #:nodoc: @@ -267,7 +282,7 @@ module ActionDispatch "integrity hash for cookie session data. Use " + "config.secret_token = \"some secret phrase of at " + "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/application.rb" + "in config/initializers/secret_token.rb" end if secret.length < SECRET_MIN_LENGTH diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 3c7dcea003..01826fcede 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,6 +1,7 @@ require 'erb' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/blank' +require 'active_support/inflector' module ActionDispatch module Routing @@ -65,6 +66,18 @@ module ActionDispatch end @options.merge!(default_controller_and_action(to_shorthand)) + + requirements.each do |name, requirement| + # segment_keys.include?(k.to_s) || k == :controller + next unless Regexp === requirement && !constraints[name] + + if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + if requirement.multiline? + raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" + end + end end # match "account/overview" @@ -112,15 +125,6 @@ module ActionDispatch @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) } - - requirements.each do |_, requirement| - if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - if requirement.multiline? - raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" - end - end end end @@ -163,10 +167,10 @@ module ActionDispatch raise ArgumentError, "missing :action" end - { :controller => controller, :action => action }.tap do |hash| - hash.delete(:controller) if hash[:controller].blank? - hash.delete(:action) if hash[:action].blank? - end + hash = {} + hash[:controller] = controller unless controller.blank? + hash[:action] = action unless action.blank? + hash end end @@ -186,8 +190,8 @@ module ActionDispatch def request_method_condition if via = @options[:via] - via = Array(via).map { |m| m.to_s.upcase } - { :request_method => Regexp.union(*via) } + via = Array(via).map { |m| m.to_s.dasherize.upcase } + { :request_method => %r[^#{via.join('|')}$] } else { } end @@ -262,6 +266,23 @@ module ActionDispatch self end + # Mount a Rack-based application to be used within the application. + # + # mount SomeRackApp, :at => "some_route" + # + # Alternatively: + # + # mount(SomeRackApp => "some_route") + # + # All mounted applications come with routing helpers to access them. + # These are named after the class specified, so for the above example + # the helper is either +some_rack_app_path+ or +some_rack_app_url+. + # To customize this helper's name, use the +:as+ option: + # + # mount(SomeRackApp => "some_route", :as => "exciting") + # + # This will generate the +exciting_path+ and +exciting_url+ helpers + # which can be used to navigate to this mounted app. def mount(app, options = nil) if options path = options.delete(:at) @@ -323,21 +344,41 @@ module ActionDispatch module HttpHelpers # Define a route that only recognizes HTTP GET. + # For supported arguments, see +match+. + # + # Example: + # + # get 'bacon', :to => 'food#bacon' def get(*args, &block) map_method(:get, *args, &block) end # Define a route that only recognizes HTTP POST. + # For supported arguments, see +match+. + # + # Example: + # + # post 'bacon', :to => 'food#bacon' def post(*args, &block) map_method(:post, *args, &block) end # Define a route that only recognizes HTTP PUT. + # For supported arguments, see +match+. + # + # Example: + # + # put 'bacon', :to => 'food#bacon' def put(*args, &block) map_method(:put, *args, &block) end - # Define a route that only recognizes HTTP DELETE. + # Define a route that only recognizes HTTP PUT. + # For supported arguments, see +match+. + # + # Example: + # + # delete 'broccoli', :to => 'food#broccoli' def delete(*args, &block) map_method(:delete, *args, &block) end @@ -398,30 +439,30 @@ module ActionDispatch # This will create a number of routes for each of the posts and comments # controller. For Admin::PostsController, Rails will create: # - # GET /admin/photos - # GET /admin/photos/new - # POST /admin/photos - # GET /admin/photos/1 - # GET /admin/photos/1/edit - # PUT /admin/photos/1 - # DELETE /admin/photos/1 + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PUT /admin/posts/1 + # DELETE /admin/posts/1 # - # If you want to route /photos (without the prefix /admin) to + # If you want to route /posts (without the prefix /admin) to # Admin::PostsController, you could use # # scope :module => "admin" do - # resources :posts, :comments + # resources :posts # end # # or, for a single case # # resources :posts, :module => "admin" # - # If you want to route /admin/photos to PostsController + # If you want to route /admin/posts to PostsController # (without the Admin:: module prefix), you could use # # scope "/admin" do - # resources :posts, :comments + # resources :posts # end # # or, for a single case @@ -432,25 +473,64 @@ module ActionDispatch # not use scope. In the last case, the following paths map to # PostsController: # - # GET /admin/photos - # GET /admin/photos/new - # POST /admin/photos - # GET /admin/photos/1 - # GET /admin/photos/1/edit - # PUT /admin/photos/1 - # DELETE /admin/photos/1 + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PUT /admin/posts/1 + # DELETE /admin/posts/1 module Scoping def initialize(*args) #:nodoc: @scope = {} super end - # Used to route <tt>/photos</tt> (without the prefix <tt>/admin</tt>) - # to Admin::PostsController: + # === Supported options + # [:module] + # If you want to route /posts (without the prefix /admin) to + # Admin::PostsController, you could use # - # scope :module => "admin" do - # resources :posts - # end + # scope :module => "admin" do + # resources :posts + # end + # + # [:path] + # If you want to prefix the route, you could use + # + # scope :path => "/admin" do + # resources :posts + # end + # + # This will prefix all of the +posts+ resource's requests with '/admin' + # + # [:as] + # Prefixes the routing helpers in this scope with the specified label. + # + # scope :as => "sekret" do + # resources :posts + # end + # + # Helpers such as +posts_path+ will now be +sekret_posts_path+ + # + # [:shallow_path] + # + # Prefixes nested shallow routes with the specified path. + # + # scope :shallow_path => "sekret" do + # resources :posts do + # resources :comments, :shallow => true + # end + # + # The +comments+ resource here will have the following routes generated for it: + # + # post_comments GET /sekret/posts/:post_id/comments(.:format) + # post_comments POST /sekret/posts/:post_id/comments(.:format) + # new_post_comment GET /sekret/posts/:post_id/comments/new(.:format) + # edit_comment GET /sekret/comments/:id/edit(.:format) + # comment GET /sekret/comments/:id(.:format) + # comment PUT /sekret/comments/:id(.:format) + # comment DELETE /sekret/comments/:id(.:format) def scope(*args) options = args.extract_options! options = options.dup @@ -487,82 +567,199 @@ module ActionDispatch @scope[:blocks] = recover[:block] end + # Scopes routes to a specific controller + # + # Example: + # controller "food" do + # match "bacon", :action => "bacon" + # end def controller(controller, options={}) options[:controller] = controller scope(options) { yield } end + # Scopes routes to a specific namespace. For example: + # + # namespace :admin do + # resources :posts + # end + # + # This generates the following routes: + # + # admin_posts GET /admin/posts(.:format) {:action=>"index", :controller=>"admin/posts"} + # admin_posts POST /admin/posts(.:format) {:action=>"create", :controller=>"admin/posts"} + # new_admin_post GET /admin/posts/new(.:format) {:action=>"new", :controller=>"admin/posts"} + # edit_admin_post GET /admin/posts/:id/edit(.:format) {:action=>"edit", :controller=>"admin/posts"} + # admin_post GET /admin/posts/:id(.:format) {:action=>"show", :controller=>"admin/posts"} + # admin_post PUT /admin/posts/:id(.:format) {:action=>"update", :controller=>"admin/posts"} + # admin_post DELETE /admin/posts/:id(.:format) {:action=>"destroy", :controller=>"admin/posts"} + # === Supported options + # + # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+ all default to the name of the namespace. + # + # [:path] + # The path prefix for the routes. + # + # namespace :admin, :path => "sekret" do + # resources :posts + # end + # + # All routes for the above +resources+ will be accessible through +/sekret/posts+, rather than +/admin/posts+ + # + # [:module] + # The namespace for the controllers. + # + # namespace :admin, :module => "sekret" do + # resources :posts + # end + # + # The +PostsController+ here should go in the +Sekret+ namespace and so it should be defined like this: + # + # class Sekret::PostsController < ApplicationController + # # code go here + # end + # + # [:as] + # Changes the name used in routing helpers for this namespace. + # + # namespace :admin, :as => "sekret" do + # resources :posts + # end + # + # Routing helpers such as +admin_posts_path+ will now be +sekret_posts_path+. + # + # [:shallow_path] + # See the +scope+ method. def namespace(path, options = {}) path = path.to_s options = { :path => path, :as => path, :module => path, :shallow_path => path, :shallow_prefix => path }.merge!(options) scope(options) { yield } end - + + # === Parameter Restriction + # Allows you to constrain the nested routes based on a set of rules. + # For instance, in order to change the routes to allow for a dot character in the +id+ parameter: + # + # constraints(:id => /\d+\.\d+) do + # resources :posts + # end + # + # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be. + # The +id+ parameter must match the constraint passed in for this example. + # + # You may use this to also resrict other parameters: + # + # resources :posts do + # constraints(:post_id => /\d+\.\d+) do + # resources :comments + # end + # + # === Restricting based on IP + # + # Routes can also be constrained to an IP or a certain range of IP addresses: + # + # constraints(:ip => /192.168.\d+.\d+/) do + # resources :posts + # end + # + # Any user connecting from the 192.168.* range will be able to see this resource, + # where as any user connecting outside of this range will be told there is no such route. + # + # === Dynamic request matching + # + # Requests to routes can be constrained based on specific critera: + # + # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do + # resources :iphones + # end + # + # You are able to move this logic out into a class if it is too complex for routes. + # This class must have a +matches?+ method defined on it which either returns +true+ + # if the user should be given access to that route, or +false+ if the user should not. + # + # class Iphone + # def self.matches(request) + # request.env["HTTP_USER_AGENT"] =~ /iPhone/ + # end + # end + # + # An expected place for this code would be +lib/constraints+. + # + # This class is then used like this: + # + # constraints(Iphone) do + # resources :iphones + # end def constraints(constraints = {}) scope(:constraints => constraints) { yield } end + # Allows you to set default parameters for a route, such as this: + # defaults :id => 'home' do + # match 'scoped_pages/(:id)', :to => 'pages#show' + # end + # Using this, the +:id+ parameter here will default to 'home'. def defaults(defaults = {}) scope(:defaults => defaults) { yield } end private - def scope_options + def scope_options #:nodoc: @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym } end - def merge_path_scope(parent, child) + def merge_path_scope(parent, child) #:nodoc: Mapper.normalize_path("#{parent}/#{child}") end - def merge_shallow_path_scope(parent, child) + def merge_shallow_path_scope(parent, child) #:nodoc: Mapper.normalize_path("#{parent}/#{child}") end - def merge_as_scope(parent, child) + def merge_as_scope(parent, child) #:nodoc: parent ? "#{parent}_#{child}" : child end - def merge_shallow_prefix_scope(parent, child) + def merge_shallow_prefix_scope(parent, child) #:nodoc: parent ? "#{parent}_#{child}" : child end - def merge_module_scope(parent, child) + def merge_module_scope(parent, child) #:nodoc: parent ? "#{parent}/#{child}" : child end - def merge_controller_scope(parent, child) + def merge_controller_scope(parent, child) #:nodoc: child end - def merge_path_names_scope(parent, child) + def merge_path_names_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_constraints_scope(parent, child) + def merge_constraints_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_defaults_scope(parent, child) + def merge_defaults_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_blocks_scope(parent, child) + def merge_blocks_scope(parent, child) #:nodoc: merged = parent ? parent.dup : [] merged << child if child merged end - def merge_options_scope(parent, child) + def merge_options_scope(parent, child) #:nodoc: (parent || {}).except(*override_keys(child)).merge(child) end - def merge_shallow_scope(parent, child) + def merge_shallow_scope(parent, child) #:nodoc: child ? true : false end - def override_keys(child) + def override_keys(child) #:nodoc: child.key?(:only) || child.key?(:except) ? [:only, :except] : [] end end @@ -770,6 +967,45 @@ module ActionDispatch # GET /photos/:id/edit # PUT /photos/:id # DELETE /photos/:id + # + # Resources can also be nested infinitely by using this block syntax: + # + # resources :photos do + # resources :comments + # end + # + # This generates the following comments routes: + # + # GET /photos/:id/comments/new + # POST /photos/:id/comments + # GET /photos/:id/comments/:id + # GET /photos/:id/comments/:id/edit + # PUT /photos/:id/comments/:id + # DELETE /photos/:id/comments/:id + # + # === Supported options + # [:path_names] + # Allows you to change the paths of the seven default actions. + # Paths not specified are not changed. + # + # resources :posts, :path_names => { :new => "brand_new" } + # + # The above example will now change /posts/new to /posts/brand_new + # + # [:module] + # Set the module where the controller can be found. Defaults to nothing. + # + # resources :posts, :module => "admin" + # + # All requests to the posts resources will now go to +Admin::PostsController+. + # + # [:path] + # + # Set a path prefix for this resource. + # + # resources :posts, :path => "admin" + # + # All actions for this resource will now be at +/admin/posts+. def resources(*resources, &block) options = resources.extract_options! @@ -960,7 +1196,7 @@ module ActionDispatch @scope[:scope_level_resource] end - def apply_common_behavior_for(method, resources, options, &block) + def apply_common_behavior_for(method, resources, options, &block) #:nodoc: if resources.length > 1 resources.each { |r| send(method, r, options, &block) } return true @@ -990,23 +1226,23 @@ module ActionDispatch false end - def action_options?(options) + def action_options?(options) #:nodoc: options[:only] || options[:except] end - def scope_action_options? + def scope_action_options? #:nodoc: @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except]) end - def scope_action_options + def scope_action_options #:nodoc: @scope[:options].slice(:only, :except) end - def resource_scope? + def resource_scope? #:nodoc: [:resource, :resources].include?(@scope[:scope_level]) end - def resource_method_scope? + def resource_method_scope? #:nodoc: [:collection, :member, :new].include?(@scope[:scope_level]) end @@ -1032,7 +1268,7 @@ module ActionDispatch @scope[:scope_level_resource] = old_resource end - def resource_scope(resource) + def resource_scope(resource) #:nodoc: with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do scope(parent_resource.resource_scope) do yield @@ -1040,30 +1276,30 @@ module ActionDispatch end end - def nested_options + def nested_options #:nodoc: {}.tap do |options| options[:as] = parent_resource.member_name options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint? end end - def id_constraint? + def id_constraint? #:nodoc: @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp) end - def id_constraint + def id_constraint #:nodoc: @scope[:constraints][:id] end - def canonical_action?(action, flag) + def canonical_action?(action, flag) #:nodoc: flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scoping? + def shallow_scoping? #:nodoc: shallow? && @scope[:scope_level] == :member end - def path_for_action(action, path) + def path_for_action(action, path) #:nodoc: prefix = shallow_scoping? ? "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path] @@ -1074,11 +1310,11 @@ module ActionDispatch end end - def action_path(name, path = nil) + def action_path(name, path = nil) #:nodoc: path || @scope[:path_names][name.to_sym] || name.to_s end - def prefix_name_for_action(as, action) + def prefix_name_for_action(as, action) #:nodoc: if as as.to_s elsif !canonical_action?(action, @scope[:scope_level]) @@ -1086,12 +1322,14 @@ module ActionDispatch end end - def name_for_action(as, action) + def name_for_action(as, action) #:nodoc: prefix = prefix_name_for_action(as, action) prefix = Mapper.normalize_name(prefix) if prefix name_prefix = @scope[:as] if parent_resource + return nil if as.nil? && action.nil? + collection_name = parent_resource.collection_name member_name = parent_resource.member_name end @@ -1116,7 +1354,7 @@ module ActionDispatch end end - module Shorthand + module Shorthand #:nodoc: def match(*args) if args.size == 1 && args.last.is_a?(Hash) options = args.pop diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb index f91a48e16c..08a8408f25 100644 --- a/actionpack/lib/action_dispatch/routing/route.rb +++ b/actionpack/lib/action_dispatch/routing/route.rb @@ -30,7 +30,8 @@ module ActionDispatch if method = conditions[:request_method] case method when Regexp - method.source.upcase + source = method.source.upcase + source =~ /\A\^[-A-Z|]+\$\Z/ ? source[1..-2] : source else method.to_s.upcase end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 32f41934f1..ebced9cabe 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -485,7 +485,8 @@ module ActionDispatch Generator.new(options, recall, self, extras).generate end - RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :script_name] + RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, + :trailing_slash, :script_name, :anchor, :params, :only_path ] def _generate_prefix(options = {}) nil @@ -504,11 +505,8 @@ module ActionDispatch rewritten_url << (options[:protocol] || "http") rewritten_url << "://" unless rewritten_url.match("://") rewritten_url << rewrite_authentication(options) - - raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] - - rewritten_url << options[:host] - rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) + rewritten_url << host_from_options(options) + rewritten_url << ":#{options.delete(:port)}" if options[:port] end script_name = options.delete(:script_name) @@ -562,6 +560,26 @@ module ActionDispatch end private + + def host_from_options(options) + computed_host = subdomain_and_domain(options) || options[:host] + unless computed_host + raise ArgumentError, "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" + end + computed_host + end + + def subdomain_and_domain(options) + return nil unless options[:subdomain] || options[:domain] + tld_length = options[:tld_length] || ActionDispatch::Http::URL.tld_length + + host = "" + host << (options[:subdomain] || ActionDispatch::Http::URL.extract_subdomain(options[:host], tld_length)) + host << "." + host << (options[:domain] || ActionDispatch::Http::URL.extract_domain(options[:host], tld_length)) + host + end + def handle_positional_args(options) return unless args = options.delete(:_positional_args) diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index bfdea41f60..d4db78a25a 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -115,6 +115,13 @@ module ActionDispatch # * <tt>:host</tt> - Specifies the host the link should be targeted at. # If <tt>:only_path</tt> is false, this option must be # provided either explicitly, or via +default_url_options+. + # * <tt>:subdomain</tt> - Specifies the subdomain of the link, using the +tld_length+ + # to split the domain from the host. + # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+ + # to split the subdomain from the host. + # * <tt>:tld_length</tt> - Number of labels the TLD id composed of, only used if + # <tt>:subdomain</tt> or <tt>:domain</tt> are supplied. Defaults to + # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1. # * <tt>:port</tt> - Optionally specify the port to connect to. # * <tt>:anchor</tt> - An anchor name to be appended to the path. # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index fee8cad9f5..8fe74c3c80 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -363,6 +363,10 @@ module ActionDispatch integration_session.url_options end + def respond_to?(method, include_private = false) + integration_session.respond_to?(method, include_private) || super + end + # Delegate unhandled messages to the current session instance. def method_missing(sym, *args, &block) reset! unless integration_session diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index c56ebc6438..16f3674164 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -22,7 +22,7 @@ module ActionDispatch end def cookies - @request.cookies.merge(@response.cookies) + HashWithIndifferentAccess.new(@request.cookies.merge(@response.cookies)) end def redirect_to_url diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index 73e16daa29..170ceb299a 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -3,8 +3,8 @@ module ActionPack MAJOR = 3 MINOR = 1 TINY = 0 - BUILD = "beta" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 1beae37af3..15944138f7 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -76,8 +76,8 @@ module ActionView #:nodoc: # # === 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. + # By default, Rails will compile each template to a method in order to render it. When you alter a template, + # Rails will check the file's modification time and recompile it in development mode. # # == Builder # diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index a728fde748..f6b2d4f3f4 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -1,10 +1,6 @@ -require 'thread' -require 'cgi' -require 'action_view/helpers/url_helper' -require 'action_view/helpers/tag_helper' -require 'active_support/core_ext/file' -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/string/output_safety' +require 'action_view/helpers/asset_tag_helpers/javascript_tag_helpers' +require 'action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers' +require 'action_view/helpers/asset_tag_helpers/asset_paths' module ActionView # = Action View Asset Tag Helpers @@ -195,20 +191,8 @@ module ActionView # RewriteEngine On # RewriteRule ^/release-\d+/(images|javascripts|stylesheets)/(.*)$ /$1/$2 [L] module AssetTagHelper - 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 - + include JavascriptTagHelpers + include StylesheetTagHelpers # Returns a link tag that browsers and news readers can use to auto-detect # an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or # <tt>:atom</tt>. Control the link options in url_for format using the @@ -242,260 +226,6 @@ module ActionView ) end - # Computes the path to a javascript asset in the public javascripts directory. - # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) - # Full paths from the document root will be passed through. - # Used internally by javascript_include_tag to build the script path. - # - # ==== Examples - # javascript_path "xmlhr" # => /javascripts/xmlhr.js - # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js - # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js - # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr - # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js - def javascript_path(source) - compute_public_path(source, 'javascripts', 'js') - end - alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route - - # Returns an HTML script tag for each of the +sources+ provided. You - # can pass in the filename (.js extension is optional) of JavaScript files - # that exist in your <tt>public/javascripts</tt> directory for inclusion into the - # current page or you can pass the full path relative to your document - # root. To include the Prototype and Scriptaculous JavaScript libraries in - # your application, pass <tt>:defaults</tt> as the source. When using - # <tt>:defaults</tt>, if an <tt>application.js</tt> file exists in - # <tt>public/javascripts</tt> it will be included as well. You can modify the - # HTML attributes of the script tag by passing a hash as the last argument. - # - # ==== Examples - # javascript_include_tag "xmlhr" # => - # <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script> - # - # javascript_include_tag "xmlhr.js" # => - # <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script> - # - # javascript_include_tag "common.javascript", "/elsewhere/cools" # => - # <script type="text/javascript" src="/javascripts/common.javascript?1284139606"></script> - # <script type="text/javascript" src="/elsewhere/cools.js?1423139606"></script> - # - # javascript_include_tag "http://www.railsapplication.com/xmlhr" # => - # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> - # - # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # => - # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> - # - # javascript_include_tag :defaults # => - # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> - # ... - # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> - # - # * = The application.js file is only referenced if it exists - # - # You can also include all javascripts in the +javascripts+ directory using <tt>:all</tt> as the source: - # - # javascript_include_tag :all # => - # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> - # ... - # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script> - # - # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to - # all subsequently included files. - # - # If you want Rails to search in all the subdirectories under javascripts, you should explicitly set <tt>:recursive</tt>: - # - # javascript_include_tag :all, :recursive => true - # - # == Caching multiple javascripts into one - # - # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be - # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching - # is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development - # environment). - # - # ==== Examples - # javascript_include_tag :all, :cache => true # when config.perform_caching is false => - # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> - # ... - # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script> - # - # javascript_include_tag :all, :cache => true # when config.perform_caching is true => - # <script type="text/javascript" src="/javascripts/all.js?1344139789"></script> - # - # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is false => - # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> - # <script type="text/javascript" src="/javascripts/cart.js?1289139157"></script> - # <script type="text/javascript" src="/javascripts/checkout.js?1299139816"></script> - # - # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is true => - # <script type="text/javascript" src="/javascripts/shop.js?1299139816"></script> - # - # The <tt>:recursive</tt> option is also available for caching: - # - # javascript_include_tag :all, :cache => true, :recursive => true - def javascript_include_tag(*sources) - options = sources.extract_options!.stringify_keys - concat = options.delete("concat") - cache = concat || options.delete("cache") - recursive = options.delete("recursive") - - if concat || (config.perform_caching && cache) - joined_javascript_name = (cache == true ? "all" : cache) + ".js" - joined_javascript_path = File.join(joined_javascript_name[/^#{File::SEPARATOR}/] ? config.assets_dir : config.javascripts_dir, joined_javascript_name) - - unless config.perform_caching && File.exists?(joined_javascript_path) - write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources, recursive)) - end - javascript_src_tag(joined_javascript_name, options) - else - sources = expand_javascript_sources(sources, recursive) - ensure_javascript_sources!(sources) if cache - sources.collect { |source| javascript_src_tag(source, options) }.join("\n").html_safe - end - end - - # 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 - # that the plugin installed in <tt>public/javascripts</tt>. - # - # ActionView::Helpers::AssetTagHelper.register_javascript_expansion :monkey => ["head", "body", "tail"] - # - # javascript_include_tag :monkey # => - # <script type="text/javascript" src="/javascripts/head.js"></script> - # <script type="text/javascript" src="/javascripts/body.js"></script> - # <script type="text/javascript" src="/javascripts/tail.js"></script> - def self.register_javascript_expansion(expansions) - @@javascript_expansions.merge!(expansions) - end - - # 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 - # that the plugin installed in <tt>public/stylesheets</tt>. - # - # ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion :monkey => ["head", "body", "tail"] - # - # stylesheet_link_tag :monkey # => - # <link href="/stylesheets/head.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" /> - def self.register_stylesheet_expansion(expansions) - @@stylesheet_expansions.merge!(expansions) - 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. - # Used internally by +stylesheet_link_tag+ to build the stylesheet path. - # - # ==== Examples - # stylesheet_path "style" # => /stylesheets/style.css - # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css - # stylesheet_path "/dir/style.css" # => /dir/style.css - # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style - # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css - def stylesheet_path(source) - compute_public_path(source, 'stylesheets', 'css') - end - alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route - - # Returns a stylesheet link tag for the sources specified as arguments. If - # you don't specify an extension, <tt>.css</tt> will be appended automatically. - # You can modify the link attributes by passing a hash as the last argument. - # - # ==== Examples - # stylesheet_link_tag "style" # => - # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "style.css" # => - # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "http://www.railsapplication.com/style.css" # => - # <link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "style", :media => "all" # => - # <link href="/stylesheets/style.css" media="all" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "style", :media => "print" # => - # <link href="/stylesheets/style.css" media="print" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "random.styles", "/css/stylish" # => - # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" /> - # - # You can also include all styles in the stylesheets directory using <tt>:all</tt> as the source: - # - # stylesheet_link_tag :all # => - # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> - # - # If you want Rails to search in all the subdirectories under stylesheets, you should explicitly set <tt>:recursive</tt>: - # - # stylesheet_link_tag :all, :recursive => true - # - # == Caching multiple stylesheets into one - # - # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be - # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching - # is set to true (which is the case by default for the Rails production environment, but not for the development - # environment). Examples: - # - # ==== Examples - # stylesheet_link_tag :all, :cache => true # when config.perform_caching is false => - # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag :all, :cache => true # when config.perform_caching is true => - # <link href="/stylesheets/all.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is false => - # <link href="/stylesheets/shop.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/cart.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/checkout.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is true => - # <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" /> - # - # The <tt>:recursive</tt> option is also available for caching: - # - # stylesheet_link_tag :all, :cache => true, :recursive => true - # - # To force concatenation (even in development mode) set <tt>:concat</tt> to true. This is useful if - # you have too many stylesheets for IE to load. - # - # stylesheet_link_tag :all, :concat => true - # - def stylesheet_link_tag(*sources) - options = sources.extract_options!.stringify_keys - concat = options.delete("concat") - cache = concat || options.delete("cache") - recursive = options.delete("recursive") - - if concat || (config.perform_caching && cache) - joined_stylesheet_name = (cache == true ? "all" : cache) + ".css" - joined_stylesheet_path = File.join(joined_stylesheet_name[/^#{File::SEPARATOR}/] ? config.assets_dir : config.stylesheets_dir, joined_stylesheet_name) - - unless config.perform_caching && File.exists?(joined_stylesheet_path) - write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources, recursive)) - end - stylesheet_tag(joined_stylesheet_name, options) - else - sources = expand_stylesheet_sources(sources, recursive) - ensure_stylesheet_sources!(sources) if cache - sources.collect { |source| stylesheet_tag(source, options) }.join("\n").html_safe - end - end - # Web browsers cache favicons. If you just throw a <tt>favicon.ico</tt> into the document # root of your application and it changes later, clients that have it in their cache # won't see the update. Using this helper prevents that because it appends an asset ID: @@ -544,7 +274,7 @@ module ActionView # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and # plugin authors are encouraged to do so. def image_path(source) - compute_public_path(source, 'images') + asset_paths.compute_public_path(source, 'images') end alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route @@ -559,7 +289,7 @@ module ActionView # video_path("/trailers/hd.avi") # => /trailers/hd.avi # video_path("http://www.railsapplication.com/vid/hd.avi") # => http://www.railsapplication.com/vid/hd.avi def video_path(source) - compute_public_path(source, 'videos') + asset_paths.compute_public_path(source, 'videos') end alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route @@ -569,12 +299,12 @@ module ActionView # # ==== Examples # audio_path("horse") # => /audios/horse - # audio_path("horse.wav") # => /audios/horse.avi - # audio_path("sounds/horse.wav") # => /audios/sounds/horse.avi - # audio_path("/sounds/horse.wav") # => /sounds/horse.avi + # audio_path("horse.wav") # => /audios/horse.wav + # audio_path("sounds/horse.wav") # => /audios/sounds/horse.wav + # audio_path("/sounds/horse.wav") # => /sounds/horse.wav # audio_path("http://www.railsapplication.com/sounds/horse.wav") # => http://www.railsapplication.com/sounds/horse.wav def audio_path(source) - compute_public_path(source, 'audios') + asset_paths.compute_public_path(source, 'audios') end alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route @@ -703,213 +433,8 @@ module ActionView private - def rewrite_extension(source, dir, ext) - source_ext = File.extname(source) - - if source_ext.empty? - "#{source}.#{ext}" - elsif ext != source_ext[1..-1] - with_ext = "#{source}.#{ext}" - with_ext if File.exist?(File.join(config.assets_dir, dir, with_ext)) - end || source - end - - def rewrite_host_and_protocol(source, has_request) - host = compute_asset_host(source) - if has_request && host && !is_uri?(host) - host = "#{controller.request.protocol}#{host}" - end - "#{host}#{source}" - end - - def rewrite_relative_url_root(source, relative_url_root) - relative_url_root && !source.starts_with?("#{relative_url_root}/") ? "#{relative_url_root}#{source}" : source - end - - # Add the the extension +ext+ if not present. Return full URLs otherwise untouched. - # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL - # roots. Rewrite the asset path for cache-busting asset ids. Include - # asset host, if configured, with the correct request protocol. - def compute_public_path(source, dir, ext = nil, include_host = true) - return source if is_uri?(source) - - source = rewrite_extension(source, dir, ext) if ext - source = "/#{dir}/#{source}" unless source[0] == ?/ - if controller.respond_to?(:env) && controller.env["action_dispatch.asset_path"] - source = rewrite_asset_path(source, controller.env["action_dispatch.asset_path"]) - end - source = rewrite_asset_path(source, config.asset_path) - - has_request = controller.respond_to?(:request) - source = rewrite_relative_url_root(source, controller.config.relative_url_root) if has_request && include_host - source = rewrite_host_and_protocol(source, has_request) if include_host - - source - end - - def is_uri?(path) - path =~ %r{^[-a-z]+://|^cid:} - end - - # Pick an asset host for this source. Returns +nil+ if no host is set, - # the host if no wildcard is set, the host interpolated with the - # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4), - # or the value returned from invoking the proc if it's a proc or the value from - # invoking call if it's an object responding to call. - def compute_asset_host(source) - if host = config.asset_host - if host.is_a?(Proc) || host.respond_to?(:call) - case host.is_a?(Proc) ? host.arity : host.method(:call).arity - when 2 - request = controller.respond_to?(:request) && controller.request - host.call(source, request) - else - host.call(source) - end - else - (host =~ /%d/) ? host % (source.hash % 4) : host - end - end - end - - @@asset_timestamps_cache = {} - @@asset_timestamps_cache_guard = Mutex.new - - # Use the RAILS_ASSET_ID environment variable or the source's - # modification time as its cache-busting asset id. - def rails_asset_id(source) - if asset_id = ENV["RAILS_ASSET_ID"] - asset_id - else - if @@cache_asset_timestamps && (asset_id = @@asset_timestamps_cache[source]) - asset_id - else - path = File.join(config.assets_dir, source) - asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : '' - - if @@cache_asset_timestamps - @@asset_timestamps_cache_guard.synchronize do - @@asset_timestamps_cache[source] = asset_id - end - end - - asset_id - end - end - end - - # Break out the asset path rewrite in case plugins wish to put the asset id - # someplace other than the query string. - def rewrite_asset_path(source, path = nil) - if path && path.respond_to?(:call) - return path.call(source) - elsif path && path.is_a?(String) - return path % [source] - end - - asset_id = rails_asset_id(source) - if asset_id.empty? - source - else - "#{source}?#{asset_id}" - end - end - - def javascript_src_tag(source, options) - content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options)) - end - - def stylesheet_tag(source, options) - tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => ERB::Util.html_escape(path_to_stylesheet(source)) }.merge(options), false, false) - end - - def compute_javascript_paths(*args) - expand_javascript_sources(*args).collect { |source| compute_public_path(source, 'javascripts', 'js', false) } - end - - def compute_stylesheet_paths(*args) - expand_stylesheet_sources(*args).collect { |source| compute_public_path(source, 'stylesheets', 'css', false) } - end - - def expand_javascript_sources(sources, recursive = false) - if sources.include?(:all) - all_javascript_files = (collect_asset_files(config.javascripts_dir, ('**' if recursive), '*.js') - ['application']) << 'application' - ((determine_source(:defaults, @@javascript_expansions).dup & all_javascript_files) + all_javascript_files).uniq - else - expanded_sources = sources.collect do |source| - determine_source(source, @@javascript_expansions) - end.flatten - expanded_sources << "application" if sources.include?(:defaults) && File.exist?(File.join(config.javascripts_dir, "application.js")) - expanded_sources - end - end - - def expand_stylesheet_sources(sources, recursive) - if sources.first == :all - collect_asset_files(config.stylesheets_dir, ('**' if recursive), '*.css') - else - sources.collect do |source| - determine_source(source, @@stylesheet_expansions) - end.flatten - end - end - - def determine_source(source, collection) - case source - when Symbol - collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}") - else - source - end - end - - def ensure_stylesheet_sources!(sources) - sources.each do |source| - asset_file_path!(path_to_stylesheet(source)) - end - return sources - end - - def ensure_javascript_sources!(sources) - sources.each do |source| - asset_file_path!(path_to_javascript(source)) - end - return sources - end - - def join_asset_file_contents(paths) - paths.collect { |path| File.read(asset_file_path!(path)) }.join("\n\n") - end - - def write_asset_file_contents(joined_asset_path, asset_paths) - - FileUtils.mkdir_p(File.dirname(joined_asset_path)) - File.atomic_write(joined_asset_path) { |cache| cache.write(join_asset_file_contents(asset_paths)) } - - # Set mtime to the latest of the combined files to allow for - # consistent ETag without a shared filesystem. - mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max - File.utime(mt, mt, joined_asset_path) - end - - def asset_file_path(path) - File.join(config.assets_dir, path.split('?').first) - end - - def asset_file_path!(path) - unless is_uri?(path) - absolute_path = asset_file_path(path) - raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path) - return absolute_path - end - end - - def collect_asset_files(*path) - dir = path.first - - Dir[File.join(*path.compact)].collect do |file| - file[-(file.size - dir.size - 1)..-1].sub(/\.\w+$/, '') - end.sort + def asset_paths + @asset_paths ||= AssetPaths.new(config, controller) end end end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb new file mode 100644 index 0000000000..e52797042f --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb @@ -0,0 +1,132 @@ +require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/file' +require 'action_view/helpers/tag_helper' + +module ActionView + module Helpers + module AssetTagHelper + + class AssetIncludeTag + attr_reader :config, :asset_paths + + class_attribute :expansions + self.expansions = { } + + def initialize(config, asset_paths) + @config = config + @asset_paths = asset_paths + end + + def asset_name + raise NotImplementedError + end + + def extension + raise NotImplementedError + end + + def custom_dir + raise NotImplementedError + end + + def asset_tag(source, options) + raise NotImplementedError + end + + def include_tag(*sources) + options = sources.extract_options!.stringify_keys + concat = options.delete("concat") + cache = concat || options.delete("cache") + recursive = options.delete("recursive") + + if concat || (config.perform_caching && cache) + joined_name = (cache == true ? "all" : cache) + ".#{extension}" + joined_path = File.join((joined_name[/^#{File::SEPARATOR}/] ? config.assets_dir : custom_dir), joined_name) + unless config.perform_caching && File.exists?(joined_path) + write_asset_file_contents(joined_path, compute_paths(sources, recursive)) + end + asset_tag(joined_name, options) + else + sources = expand_sources(sources, recursive) + ensure_sources!(sources) if cache + sources.collect { |source| asset_tag(source, options) }.join("\n").html_safe + end + end + + + private + + def path_to_asset(source, include_host = true) + asset_paths.compute_public_path(source, asset_name.to_s.pluralize, extension, include_host) + end + + def compute_paths(*args) + expand_sources(*args).collect { |source| asset_paths.compute_public_path(source, asset_name.pluralize, extension, false) } + end + + def expand_sources(sources, recursive) + if sources.first == :all + collect_asset_files(custom_dir, ('**' if recursive), "*.#{extension}") + else + sources.collect do |source| + determine_source(source, expansions) + end.flatten + end + end + + def ensure_sources!(sources) + sources.each do |source| + asset_file_path!(path_to_asset(source, false)) + end + end + + def collect_asset_files(*path) + dir = path.first + + Dir[File.join(*path.compact)].collect do |file| + file[-(file.size - dir.size - 1)..-1].sub(/\.\w+$/, '') + end.sort + end + + def determine_source(source, collection) + case source + when Symbol + collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}") + else + source + end + end + + def join_asset_file_contents(paths) + paths.collect { |path| File.read(asset_file_path!(path, true)) }.join("\n\n") + end + + def write_asset_file_contents(joined_asset_path, asset_paths) + FileUtils.mkdir_p(File.dirname(joined_asset_path)) + File.atomic_write(joined_asset_path) { |cache| cache.write(join_asset_file_contents(asset_paths)) } + + # Set mtime to the latest of the combined files to allow for + # consistent ETag without a shared filesystem. + mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max + File.utime(mt, mt, joined_asset_path) + end + + def asset_file_path(path) + File.join(config.assets_dir, path.split('?').first) + end + + def asset_file_path!(path, error_if_file_is_uri = false) + if asset_paths.is_uri?(path) + raise(Errno::ENOENT, "Asset file #{path} is uri and cannot be merged into single file") if error_if_file_is_uri + else + absolute_path = asset_file_path(path) + raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path) + return absolute_path + end + end + end + + end + end +end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb new file mode 100644 index 0000000000..b4e61f2034 --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb @@ -0,0 +1,153 @@ +require 'active_support/core_ext/file' + +module ActionView + module Helpers + module AssetTagHelper + + class AssetPaths + # You can enable or disable the asset tag ids cache. + # With the cache enabled, the asset tag helper methods will make fewer + # expensive file system calls (the default implementation checks the file + # system timestamp). However this prevents you from modifying any asset + # files while the server is running. + # + # ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false + mattr_accessor :cache_asset_ids + + attr_reader :config, :controller + + def initialize(config, controller) + @config = config + @controller = controller + end + + # Add the the extension +ext+ if not present. Return full URLs otherwise untouched. + # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL + # roots. Rewrite the asset path for cache-busting asset ids. Include + # asset host, if configured, with the correct request protocol. + def compute_public_path(source, dir, ext = nil, include_host = true) + return source if is_uri?(source) + + source = rewrite_extension(source, dir, ext) if ext + source = "/#{dir}/#{source}" unless source[0] == ?/ + if controller.respond_to?(:env) && controller.env["action_dispatch.asset_path"] + source = rewrite_asset_path(source, controller.env["action_dispatch.asset_path"]) + end + source = rewrite_asset_path(source, config.asset_path) + + has_request = controller.respond_to?(:request) + source = rewrite_relative_url_root(source, controller.config.relative_url_root) if has_request && include_host + source = rewrite_host_and_protocol(source, has_request) if include_host + + source + end + + # Add or change an asset id in the asset id cache. This can be used + # for SASS on Heroku. + # :api: public + def add_to_asset_ids_cache(source, asset_id) + self.asset_ids_cache_guard.synchronize do + self.asset_ids_cache[source] = asset_id + end + end + + def is_uri?(path) + path =~ %r{^[-a-z]+://|^cid:} + end + + private + + def rewrite_extension(source, dir, ext) + source_ext = File.extname(source) + + source_with_ext = if source_ext.empty? + "#{source}.#{ext}" + elsif ext != source_ext[1..-1] + with_ext = "#{source}.#{ext}" + with_ext if File.exist?(File.join(config.assets_dir, dir, with_ext)) + end + + source_with_ext || source + end + + # Break out the asset path rewrite in case plugins wish to put the asset id + # someplace other than the query string. + def rewrite_asset_path(source, path = nil) + if path && path.respond_to?(:call) + return path.call(source) + elsif path && path.is_a?(String) + return path % [source] + end + + asset_id = rails_asset_id(source) + if asset_id.empty? + source + else + "#{source}?#{asset_id}" + end + end + + mattr_accessor :asset_ids_cache + self.asset_ids_cache = {} + + mattr_accessor :asset_ids_cache_guard + self.asset_ids_cache_guard = Mutex.new + + # Use the RAILS_ASSET_ID environment variable or the source's + # modification time as its cache-busting asset id. + def rails_asset_id(source) + if asset_id = ENV["RAILS_ASSET_ID"] + asset_id + else + if self.cache_asset_ids && (asset_id = self.asset_ids_cache[source]) + asset_id + else + path = File.join(config.assets_dir, source) + asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : '' + + if self.cache_asset_ids + add_to_asset_ids_cache(source, asset_id) + end + + asset_id + end + end + end + + def rewrite_relative_url_root(source, relative_url_root) + relative_url_root && !source.starts_with?("#{relative_url_root}/") ? "#{relative_url_root}#{source}" : source + end + + def rewrite_host_and_protocol(source, has_request) + host = compute_asset_host(source) + if has_request && host && !is_uri?(host) + host = "#{controller.request.protocol}#{host}" + end + "#{host}#{source}" + end + + # Pick an asset host for this source. Returns +nil+ if no host is set, + # the host if no wildcard is set, the host interpolated with the + # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4), + # or the value returned from invoking the proc if it's a proc or the value from + # invoking call if it's an object responding to call. + def compute_asset_host(source) + if host = config.asset_host + if host.is_a?(Proc) || host.respond_to?(:call) + case host.is_a?(Proc) ? host.arity : host.method(:call).arity + when 2 + request = controller.respond_to?(:request) && controller.request + host.call(source, request) + else + host.call(source) + end + else + (host =~ /%d/) ? host % (source.hash % 4) : host + end + end + end + end + + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb new file mode 100644 index 0000000000..6581e1d6f2 --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb @@ -0,0 +1,173 @@ +require 'active_support/concern' +require 'active_support/core_ext/file' +require 'action_view/helpers/tag_helper' +require 'action_view/helpers/asset_tag_helpers/asset_include_tag' + +module ActionView + module Helpers + module AssetTagHelper + + class JavascriptIncludeTag < AssetIncludeTag + include TagHelper + + def asset_name + 'javascript' + end + + def extension + 'js' + end + + def asset_tag(source, options) + content_tag("script", "", { "type" => Mime::JS, "src" => path_to_asset(source) }.merge(options)) + end + + def custom_dir + config.javascripts_dir + end + + private + + def expand_sources(sources, recursive = false) + if sources.include?(:all) + all_asset_files = (collect_asset_files(custom_dir, ('**' if recursive), "*.#{extension}") - ['application']) << 'application' + ((determine_source(:defaults, expansions).dup & all_asset_files) + all_asset_files).uniq + else + expanded_sources = sources.collect do |source| + determine_source(source, expansions) + end.flatten + expanded_sources << "application" if sources.include?(:defaults) && File.exist?(File.join(custom_dir, "application.#{extension}")) + expanded_sources + end + end + end + + + module JavascriptTagHelpers + extend ActiveSupport::Concern + + module ClassMethods + # 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 + # that the plugin installed in <tt>public/javascripts</tt>. + # + # ActionView::Helpers::AssetTagHelper.register_javascript_expansion :monkey => ["head", "body", "tail"] + # + # javascript_include_tag :monkey # => + # <script type="text/javascript" src="/javascripts/head.js"></script> + # <script type="text/javascript" src="/javascripts/body.js"></script> + # <script type="text/javascript" src="/javascripts/tail.js"></script> + def register_javascript_expansion(expansions) + JavascriptIncludeTag.expansions.merge!(expansions) + end + end + + # Computes the path to a javascript asset in the public javascripts directory. + # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) + # Full paths from the document root will be passed through. + # Used internally by javascript_include_tag to build the script path. + # + # ==== Examples + # javascript_path "xmlhr" # => /javascripts/xmlhr.js + # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js + # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js + # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr + # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js + def javascript_path(source) + asset_paths.compute_public_path(source, 'javascripts', 'js') + end + alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route + + # Returns an HTML script tag for each of the +sources+ provided. You + # can pass in the filename (.js extension is optional) of JavaScript files + # that exist in your <tt>public/javascripts</tt> directory for inclusion into the + # current page or you can pass the full path relative to your document + # root. To include the Prototype and Scriptaculous JavaScript libraries in + # your application, pass <tt>:defaults</tt> as the source. When using + # <tt>:defaults</tt>, if an <tt>application.js</tt> file exists in + # <tt>public/javascripts</tt> it will be included as well. You can modify the + # HTML attributes of the script tag by passing a hash as the last argument. + # + # ==== Examples + # javascript_include_tag "xmlhr" # => + # <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script> + # + # javascript_include_tag "xmlhr.js" # => + # <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script> + # + # javascript_include_tag "common.javascript", "/elsewhere/cools" # => + # <script type="text/javascript" src="/javascripts/common.javascript?1284139606"></script> + # <script type="text/javascript" src="/elsewhere/cools.js?1423139606"></script> + # + # javascript_include_tag "http://www.railsapplication.com/xmlhr" # => + # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> + # + # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # => + # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> + # + # javascript_include_tag :defaults # => + # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> + # ... + # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> + # + # * = The application.js file is only referenced if it exists + # + # You can also include all javascripts in the +javascripts+ directory using <tt>:all</tt> as the source: + # + # javascript_include_tag :all # => + # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> + # ... + # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script> + # + # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to + # all subsequently included files. + # + # If you want Rails to search in all the subdirectories under javascripts, you should explicitly set <tt>:recursive</tt>: + # + # javascript_include_tag :all, :recursive => true + # + # == Caching multiple javascripts into one + # + # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be + # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching + # is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development + # environment). + # + # ==== Examples + # javascript_include_tag :all, :cache => true # when config.perform_caching is false => + # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> + # ... + # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script> + # + # javascript_include_tag :all, :cache => true # when config.perform_caching is true => + # <script type="text/javascript" src="/javascripts/all.js?1344139789"></script> + # + # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is false => + # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/cart.js?1289139157"></script> + # <script type="text/javascript" src="/javascripts/checkout.js?1299139816"></script> + # + # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is true => + # <script type="text/javascript" src="/javascripts/shop.js?1299139816"></script> + # + # The <tt>:recursive</tt> option is also available for caching: + # + # javascript_include_tag :all, :cache => true, :recursive => true + def javascript_include_tag(*sources) + @javascript_include ||= JavascriptIncludeTag.new(config, asset_paths) + @javascript_include.include_tag(*sources) + end + + end + + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb new file mode 100644 index 0000000000..d02b28d7f6 --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb @@ -0,0 +1,144 @@ +require 'active_support/concern' +require 'active_support/core_ext/file' +require 'action_view/helpers/tag_helper' +require 'action_view/helpers/asset_tag_helpers/asset_include_tag' + +module ActionView + module Helpers + module AssetTagHelper + + class StylesheetIncludeTag < AssetIncludeTag + include TagHelper + + def asset_name + 'stylesheet' + end + + def extension + 'css' + end + + def asset_tag(source, options) + tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => ERB::Util.html_escape(path_to_asset(source)) }.merge(options), false, false) + end + + def custom_dir + config.stylesheets_dir + end + end + + + module StylesheetTagHelpers + extend ActiveSupport::Concern + + module ClassMethods + # 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 + # that the plugin installed in <tt>public/stylesheets</tt>. + # + # ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion :monkey => ["head", "body", "tail"] + # + # stylesheet_link_tag :monkey # => + # <link href="/stylesheets/head.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" /> + def register_stylesheet_expansion(expansions) + StylesheetIncludeTag.expansions.merge!(expansions) + end + 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. + # Used internally by +stylesheet_link_tag+ to build the stylesheet path. + # + # ==== Examples + # stylesheet_path "style" # => /stylesheets/style.css + # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css + # stylesheet_path "/dir/style.css" # => /dir/style.css + # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style + # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css + def stylesheet_path(source) + asset_paths.compute_public_path(source, 'stylesheets', 'css') + end + alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route + + # Returns a stylesheet link tag for the sources specified as arguments. If + # you don't specify an extension, <tt>.css</tt> will be appended automatically. + # You can modify the link attributes by passing a hash as the last argument. + # + # ==== Examples + # stylesheet_link_tag "style" # => + # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "style.css" # => + # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "http://www.railsapplication.com/style.css" # => + # <link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "style", :media => "all" # => + # <link href="/stylesheets/style.css" media="all" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "style", :media => "print" # => + # <link href="/stylesheets/style.css" media="print" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "random.styles", "/css/stylish" # => + # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" /> + # + # You can also include all styles in the stylesheets directory using <tt>:all</tt> as the source: + # + # stylesheet_link_tag :all # => + # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> + # + # If you want Rails to search in all the subdirectories under stylesheets, you should explicitly set <tt>:recursive</tt>: + # + # stylesheet_link_tag :all, :recursive => true + # + # == Caching multiple stylesheets into one + # + # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be + # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching + # is set to true (which is the case by default for the Rails production environment, but not for the development + # environment). Examples: + # + # ==== Examples + # stylesheet_link_tag :all, :cache => true # when config.perform_caching is false => + # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag :all, :cache => true # when config.perform_caching is true => + # <link href="/stylesheets/all.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is false => + # <link href="/stylesheets/shop.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/cart.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/checkout.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is true => + # <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" /> + # + # The <tt>:recursive</tt> option is also available for caching: + # + # stylesheet_link_tag :all, :cache => true, :recursive => true + # + # To force concatenation (even in development mode) set <tt>:concat</tt> to true. This is useful if + # you have too many stylesheets for IE to load. + # + # stylesheet_link_tag :all, :concat => true + # + def stylesheet_link_tag(*sources) + @stylesheet_include ||= StylesheetIncludeTag.new(config, asset_paths) + @stylesheet_include.include_tag(*sources) + end + + end + + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 0401e6a09b..c88bd1efd5 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/output_safety' module ActionView # = Action View Capture Helper @@ -38,7 +39,7 @@ module ActionView value = nil buffer = with_output_buffer { value = yield(*args) } if string = buffer.presence || value and string.is_a?(String) - string + ERB::Util.html_escape string end end diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index c1214bc44e..875ec9b77b 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -882,6 +882,8 @@ module ActionView # Returns the separator for a given datetime component def separator(type) case type + when :year + @options[:discard_year] ? "" : @options[:date_separator] when :month @options[:discard_month] ? "" : @options[:date_separator] when :day diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index d6e175c7e8..ef5bbd8ae3 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -2,7 +2,7 @@ require 'cgi' require 'action_view/helpers/date_helper' require 'action_view/helpers/tag_helper' require 'action_view/helpers/form_tag_helper' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/output_safety' @@ -112,6 +112,7 @@ module ActionView # <%= f.text_field :version %><br /> # <%= f.label :author, 'Author' %>: # <%= f.text_field :author %><br /> + # <%= f.submit %> # <% end %> # # There, +form_for+ is able to generate the rest of RESTful form @@ -129,6 +130,7 @@ module ActionView # Last name : <%= f.text_field :last_name %><br /> # Biography : <%= f.text_area :biography %><br /> # Admin? : <%= f.check_box :admin %><br /> + # <%= f.submit %> # <% end %> # # There, the argument is a symbol or string with the name of the @@ -160,6 +162,7 @@ module ActionView # Last name : <%= f.text_field :last_name %> # Biography : <%= text_area :person, :biography %> # Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %> + # <%= f.submit %> # <% end %> # # This also works for the methods in FormOptionHelper and DateHelper that @@ -269,8 +272,9 @@ module ActionView # <%= form_for @person, :url => { :action => "create" }, :builder => LabellingFormBuilder do |f| %> # <%= f.text_field :first_name %> # <%= f.text_field :last_name %> - # <%= text_area :person, :biography %> - # <%= check_box_tag "person[admin]", @person.company.admin? %> + # <%= f.text_area :biography %> + # <%= f.check_box :admin %> + # <%= f.submit %> # <% end %> # # In this case, if you use this: @@ -294,10 +298,9 @@ module ActionView # # If you don't need to attach a form to a model instance, then check out # FormTagHelper#form_tag. - def form_for(record, options = nil, &proc) + def form_for(record, options = {}, &proc) raise ArgumentError, "Missing block" unless block_given? - options ||= {} options[:html] ||= {} case record @@ -348,6 +351,8 @@ module ActionView # <%= fields_for @person.permission do |permission_fields| %> # Admin? : <%= permission_fields.check_box :admin %> # <% end %> + # + # <%= f.submit %> # <% end %> # # ...or if you have an object that needs to be represented as a different @@ -409,6 +414,7 @@ module ActionView # Street : <%= address_fields.text_field :street %> # Zip code: <%= address_fields.text_field :zip_code %> # <% end %> + # ... # <% end %> # # When address is already an association on a Person you can use @@ -438,6 +444,7 @@ module ActionView # ... # Delete: <%= address_fields.check_box :_destroy %> # <% end %> + # ... # <% end %> # # ==== One-to-many @@ -467,6 +474,7 @@ module ActionView # Name: <%= project_fields.text_field :name %> # <% end %> # <% end %> + # ... # <% end %> # # It's also possible to specify the instance to be used: @@ -480,6 +488,7 @@ module ActionView # <% end %> # <% end %> # <% end %> + # ... # <% end %> # # Or a collection to be used: @@ -489,6 +498,7 @@ module ActionView # <%= person_form.fields_for :projects, @active_projects do |project_fields| %> # Name: <%= project_fields.text_field :name %> # <% end %> + # ... # <% end %> # # When projects is already an association on Person you can use @@ -518,6 +528,7 @@ module ActionView # <%= person_form.fields_for :projects do |project_fields| %> # Delete: <%= project_fields.check_box :_destroy %> # <% end %> + # ... # <% end %> def fields_for(record, record_object = nil, options = nil, &block) capture(instantiate_builder(record, record_object, options, &block), &block) @@ -1106,7 +1117,7 @@ module ActionView class FormBuilder #:nodoc: # The methods which wrap a form helper call. - class_inheritable_accessor :field_helpers + class_attribute :field_helpers self.field_helpers = (FormHelper.instance_method_names - ['form_for']) attr_accessor :object_name, :object, :options @@ -1206,6 +1217,7 @@ module ActionView self.multipart = true @template.file_field(@object_name, method, objectify_options(options)) end + # Add the submit button for the given form. When no value is given, it checks # if the object is a new resource or not to create the proper label: # @@ -1278,11 +1290,11 @@ module ActionView if association.respond_to?(:persisted?) association = [association] if @object.send(association_name).is_a?(Array) - elsif !association.is_a?(Array) + elsif !association.respond_to?(:to_ary) association = @object.send(association_name) end - if association.is_a?(Array) + if association.respond_to?(:to_ary) explicit_child_index = options[:child_index] output = ActiveSupport::SafeBuffer.new association.each do |child| diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 92645f5cf9..50f065f03d 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -39,7 +39,7 @@ module ActionView # form_tag('/upload', :multipart => true) # # => <form action="/upload" method="post" enctype="multipart/form-data"> # - # <%= form_tag('/posts')do -%> + # <%= form_tag('/posts') do -%> # <div><%= submit_tag 'Save' %></div> # <% end -%> # # => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form> diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 1488e72c6e..a9400c347f 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -15,7 +15,7 @@ module ActionView # unchanged if can't be converted into a valid number. module NumberHelper - DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :unit => "$", :separator => ".", :delimiter => ",", + DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :negative_format => "-%u%n", :unit => "$", :separator => ".", :delimiter => ",", :precision => 2, :significant => false, :strip_insignificant_zeros => false } # Raised when argument +number+ param given to the helpers is invalid and @@ -81,15 +81,18 @@ module ActionView # in the +options+ hash. # # ==== Options - # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). - # * <tt>:precision</tt> - Sets the level of precision (defaults to 2). - # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$"). - # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). - # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ","). - # * <tt>:format</tt> - Sets the format of the output string (defaults to "%u%n"). The field types are: - # - # %u The currency unit - # %n The number + # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). + # * <tt>:precision</tt> - Sets the level of precision (defaults to 2). + # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$"). + # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ","). + # * <tt>:format</tt> - Sets the format for non-negative numbers (defaults to "%u%n"). + # Fields are <tt>%u</tt> for the currency, and <tt>%n</tt> + # for the number. + # * <tt>:negative_format</tt> - Sets the format for negative numbers (defaults to prepending + # an hyphen to the formatted number given by <tt>:format</tt>). + # Accepts the same fields than <tt>:format</tt>, except + # <tt>%n</tt> is here the absolute value of the number. # # ==== Examples # number_to_currency(1234567890.50) # => $1,234,567,890.50 @@ -97,6 +100,8 @@ module ActionView # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506 # number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,506 € # + # number_to_currency(1234567890.50, :negative_format => "(%u%n)") + # # => ($1,234,567,890.51) # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "") # # => £1234567890,50 # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") @@ -110,11 +115,17 @@ module ActionView currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :default => {}) defaults = DEFAULT_CURRENCY_VALUES.merge(defaults).merge!(currency) + defaults[:negative_format] = "-" + options[:format] if options[:format] options = defaults.merge!(options) unit = options.delete(:unit) format = options.delete(:format) + if number.to_f < 0 + format = options.delete(:negative_format) + number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '') + end + begin value = number_with_precision(number, options.merge(:raise => true)) format.gsub(/%n/, value).gsub(/%u/, unit).html_safe diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 7c877a0f57..3d276000a1 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -9,6 +9,24 @@ module ActionView # and transforming strings, which can reduce the amount of inline Ruby code in # your views. These helper methods extend Action View making them callable # within your template files. + # + # ==== Sanitization + # + # Most text helpers by default sanitize the given content, but do not escape it. + # This means HTML tags will appear in the page but all malicious code will be removed. + # Let's look at some examples using the +simple_format+ method: + # + # simple_format('<a href="http://example.com/">Example</a>') + # # => "<p><a href=\"http://example.com/\">Example</a></p>" + # + # simple_format('<a href="javascript:alert('no!')">Example</a>') + # # => "<p><a>Example</a></p>" + # + # If you want to escape all content, you should invoke the +h+ method before + # calling the text helper. + # + # simple_format h('<a href="http://example.com/">Example</a>') + # # => "<p><a href=\"http://example.com/\">Example</a></p>" module TextHelper extend ActiveSupport::Concern diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb index 844c3e9572..cd3f130dc4 100644 --- a/actionpack/lib/action_view/partials.rb +++ b/actionpack/lib/action_view/partials.rb @@ -75,6 +75,11 @@ module ActionView # # <%= render :partial => "ad", :collection => @advertisements, :spacer_template => "ad_divider" %> # + # If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you + # to specify a text which will displayed instead by using this form: + # + # <%= render(:partial => "ad", :collection => @advertisements) || "There's no ad to be displayed" %> + # # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also # just keep domain objects, like Active Records, in there. # diff --git a/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb index 33dfcbb803..71cd1a788a 100644 --- a/actionpack/lib/action_view/railtie.rb +++ b/actionpack/lib/action_view/railtie.rb @@ -8,10 +8,10 @@ module ActionView config.action_view.stylesheet_expansions = {} config.action_view.javascript_expansions = { :defaults => ['prototype', 'effects', 'dragdrop', 'controls', 'rails'] } - initializer "action_view.cache_asset_timestamps" do |app| + initializer "action_view.cache_asset_ids" do |app| unless app.config.cache_classes ActiveSupport.on_load(:action_view) do - ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false + ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false end end end diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index c580397cad..317479ad4c 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -90,13 +90,14 @@ module ActionView def collection if @options.key?(:collection) - @options[:collection] || [] + collection = @options[:collection] + collection.respond_to?(:to_ary) ? collection.to_ary : [] end end def collection_from_object if @object.respond_to?(:to_ary) - @object + @object.to_ary end end @@ -163,4 +164,4 @@ module ActionView [variable, variable_counter] end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index dbb1532cd4..831a19654e 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -184,6 +184,11 @@ module ActionView end end + # Used to store template data by template handlers. + def data + @data ||= {} + end + def inspect @inspect ||= if defined?(Rails.root) @@ -269,7 +274,8 @@ module ActionView end end - code = @handler.call(self) + arity = @handler.respond_to?(:arity) ? @handler.arity : @handler.method(:call).arity + code = arity == 1 ? @handler.call(self) : @handler.call(self, view) # Make sure that the resulting String to be evalled is in the # encoding of the code diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index b827610456..0803114f44 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -15,6 +15,7 @@ module ActionView super(value.to_s) end alias :append= :<< + alias :safe_append= :safe_concat end class Template @@ -40,7 +41,11 @@ module ActionView end def add_expr_escaped(src, code) - src << '@output_buffer.append= ' << escaped_expr(code) << ';' + if code =~ BLOCK_EXPR + src << "@output_buffer.safe_append= " << code + else + src << "@output_buffer.safe_concat((" << code << ").to_s);" + end end def add_postamble(src) diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 470b36dbe2..60534a9746 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -36,17 +36,14 @@ require 'active_record' require 'action_controller/caching' require 'action_controller/caching/sweeping' -begin - require 'ruby-debug' - Debugger.settings[:autoeval] = true - Debugger.start -rescue LoadError - # Debugging disabled. `gem install ruby-debug` to enable. -end - require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late module Rails + class << self + def env + @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test") + end + end end ActiveSupport::Dependencies.hook! diff --git a/actionpack/test/activerecord/active_record_store_test.rb b/actionpack/test/activerecord/active_record_store_test.rb index f5811a1530..f0fb113860 100644 --- a/actionpack/test/activerecord/active_record_store_test.rb +++ b/actionpack/test/activerecord/active_record_store_test.rb @@ -23,6 +23,13 @@ class ActiveRecordStoreTest < ActionDispatch::IntegrationTest def call_reset_session session[:foo] reset_session + reset_session if params[:twice] + session[:foo] = "baz" + head :ok + end + + def renew + env["rack.session.options"][:renew] = true session[:foo] = "baz" head :ok end @@ -64,6 +71,20 @@ class ActiveRecordStoreTest < ActionDispatch::IntegrationTest end end end + + define_method("test_renewing_with_#{class_name}_store") do + with_store class_name do + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + + get '/renew' + assert_response :success + assert_not_equal [], headers['Set-Cookie'] + end + end + end end def test_getting_nil_session_value @@ -74,6 +95,17 @@ class ActiveRecordStoreTest < ActionDispatch::IntegrationTest end end + def test_calling_reset_session_twice_does_not_raise_errors + with_test_route_set do + get '/call_reset_session', :twice => "true" + assert_response :success + + get '/get_session_value' + assert_response :success + assert_equal 'foo: "baz"', response.body + end + end + def test_setting_session_value_after_session_reset with_test_route_set do get '/set_session_value' diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 5a210aa9ec..7f3d943bba 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -171,6 +171,14 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase end end + def test_string_constraint + with_routing do |set| + set.draw do + match "photos", :to => 'action_pack_assertions#nothing', :constraints => {:subdomain => "admin"} + end + end + end + def test_assert_redirect_to_named_route_failure with_routing do |set| set.draw do diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 3a8a37d967..68febf425d 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -78,7 +78,8 @@ class FilterTest < ActionController::TestCase end class RenderingController < ActionController::Base - before_filter :render_something_else + before_filter :before_filter_rendering + after_filter :unreached_after_filter def show @ran_action = true @@ -86,9 +87,59 @@ class FilterTest < ActionController::TestCase end private - def render_something_else + def before_filter_rendering + @ran_filter ||= [] + @ran_filter << "before_filter_rendering" render :inline => "something else" end + + def unreached_after_filter + @ran_filter << "unreached_after_filter_after_render" + end + end + + class RenderingForPrependAfterFilterController < RenderingController + prepend_after_filter :unreached_prepend_after_filter + + private + def unreached_prepend_after_filter + @ran_filter << "unreached_preprend_after_filter_after_render" + end + end + + class BeforeFilterRedirectionController < ActionController::Base + before_filter :before_filter_redirects + after_filter :unreached_after_filter + + def show + @ran_action = true + render :inline => "ran show action" + end + + def target_of_redirection + @ran_target_of_redirection = true + render :inline => "ran target_of_redirection action" + end + + private + def before_filter_redirects + @ran_filter ||= [] + @ran_filter << "before_filter_redirects" + redirect_to(:action => 'target_of_redirection') + end + + def unreached_after_filter + @ran_filter << "unreached_after_filter_after_redirection" + end + end + + class BeforeFilterRedirectionForPrependAfterFilterController < BeforeFilterRedirectionController + prepend_after_filter :unreached_prepend_after_filter_after_redirection + + private + def unreached_prepend_after_filter_after_redirection + @ran_filter << "unreached_prepend_after_filter_after_redirection" + end end class ConditionalFilterController < ActionController::Base @@ -625,6 +676,32 @@ class FilterTest < ActionController::TestCase assert !assigns["ran_action"] end + def test_before_filter_rendering_breaks_filtering_chain_for_after_filter + response = test_process(RenderingController) + assert_equal %w( before_filter_rendering ), assigns["ran_filter"] + assert !assigns["ran_action"] + end + + def test_before_filter_redirects_breaks_filtering_chain_for_after_filter + response = test_process(BeforeFilterRedirectionController) + assert_response :redirect + assert_equal "http://test.host/filter_test/before_filter_redirection/target_of_redirection", redirect_to_url + assert_equal %w( before_filter_redirects ), assigns["ran_filter"] + end + + def test_before_filter_rendering_breaks_filtering_chain_for_preprend_after_filter + response = test_process(RenderingForPrependAfterFilterController) + assert_equal %w( before_filter_rendering ), assigns["ran_filter"] + assert !assigns["ran_action"] + end + + def test_before_filter_redirects_breaks_filtering_chain_for_preprend_after_filter + response = test_process(BeforeFilterRedirectionForPrependAfterFilterController) + assert_response :redirect + assert_equal "http://test.host/filter_test/before_filter_redirection_for_prepend_after_filter/target_of_redirection", redirect_to_url + assert_equal %w( before_filter_redirects ), assigns["ran_filter"] + end + def test_filters_with_mixed_specialization_run_in_order assert_nothing_raised do response = test_process(MixedSpecializationController, 'bar') diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index a898ef76e5..82969b2979 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -2,6 +2,14 @@ require 'abstract_unit' require 'controller/fake_models' require 'active_support/core_ext/hash/conversions' +class StarStarMimeController < ActionController::Base + layout nil + + def index + render + end +end + class RespondToController < ActionController::Base layout :set_layout @@ -89,7 +97,6 @@ class RespondToController < ActionController::Base end end - Mime::Type.register("text/x-mobile", :mobile) def custom_constant_handling respond_to do |type| @@ -126,7 +133,6 @@ class RespondToController < ActionController::Base end end - Mime::Type.register_alias("text/html", :iphone) def iphone_with_html_response_type request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone" @@ -160,16 +166,43 @@ class RespondToController < ActionController::Base end end +class StarStarMimeControllerTest < ActionController::TestCase + tests StarStarMimeController + + def test_javascript_with_format + @request.accept = "text/javascript" + get :index, :format => 'js' + assert_match "function addition(a,b){ return a+b; }", @response.body + end + + def test_javascript_with_no_format + @request.accept = "text/javascript" + get :index + assert_match "function addition(a,b){ return a+b; }", @response.body + end + + def test_javascript_with_no_format_only_star_star + @request.accept = "*/*" + get :index + assert_match "function addition(a,b){ return a+b; }", @response.body + end + +end + class RespondToControllerTest < ActionController::TestCase tests RespondToController def setup super @request.host = "www.example.com" + Mime::Type.register_alias("text/html", :iphone) + Mime::Type.register("text/x-mobile", :mobile) end def teardown super + Mime::Type.unregister('text/x-mobile', :iphone) + Mime::Type.unregister('text/iphone', :mobile) end def test_html @@ -216,6 +249,16 @@ class RespondToControllerTest < ActionController::TestCase assert_response 406 end + def test_json_or_yaml_with_leading_star_star + @request.accept = "*/*, application/json" + get :json_xml_or_html + assert_equal 'HTML', @response.body + + @request.accept = "*/* , application/json" + get :json_xml_or_html + assert_equal 'HTML', @response.body + end + def test_json_or_yaml xhr :get, :json_or_yaml assert_equal 'JSON', @response.body @@ -487,6 +530,10 @@ class RespondWithController < ActionController::Base respond_with(resource) end + def using_hash_resource + respond_with({:result => resource}) + end + def using_resource_with_block respond_with(resource) do |format| format.csv { render :text => "CSV" } @@ -552,6 +599,17 @@ class InheritedRespondWithController < RespondWithController end end +class RenderJsonRespondWithController < RespondWithController + clear_respond_to + respond_to :json + + def index + respond_with(resource) do |format| + format.json { render :json => RenderJsonTestException.new('boom') } + end + end +end + class EmptyRespondWithController < ActionController::Base def index respond_with(Customer.new("david", 13)) @@ -568,6 +626,8 @@ class RespondWithControllerTest < ActionController::TestCase def teardown super + Mime::Type.unregister('text/x-mobile', :iphone) + Mime::Type.unregister('text/iphone', :mobile) end def test_using_resource @@ -587,6 +647,18 @@ class RespondWithControllerTest < ActionController::TestCase end end + def test_using_hash_resource + @request.accept = "application/xml" + get :using_hash_resource + assert_equal "application/xml", @response.content_type + assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n <name>david</name>\n</hash>\n", @response.body + + @request.accept = "application/json" + get :using_hash_resource + assert_equal "application/json", @response.content_type + assert_equal %Q[{"result":["david",13]}], @response.body + end + def test_using_resource_with_block @request.accept = "*/*" get :using_resource_with_block @@ -853,6 +925,13 @@ class RespondWithControllerTest < ActionController::TestCase assert_equal "JSON", @response.body end + def test_render_json_object_responds_to_str_still_produce_json + @controller = RenderJsonRespondWithController.new + @request.accept = "application/json" + get :index, :format => :json + assert_equal %Q{{"message":"boom","error":"RenderJsonTestException"}}, @response.body + end + def test_no_double_render_is_raised @request.accept = "text/html" assert_raise ActionView::MissingTemplate do @@ -936,6 +1015,13 @@ class MimeControllerLayoutsTest < ActionController::TestCase def setup super @request.host = "www.example.com" + Mime::Type.register_alias("text/html", :iphone) + end + + def teardown + super + Mime::Type.unregister('text/x-mobile', :iphone) + Mime::Type.unregister('text/iphone', :mobile) end def test_missing_layout_renders_properly diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index 44922cecff..543c02b2c5 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -39,4 +39,11 @@ module BareMetalTest assert_equal 404, status end end + + class BareControllerTest < ActionController::TestCase + test "GET index" do + get :index + assert_equal "Hello world", @response.body + end + end end diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb index d31193a063..584f2d772c 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -4,15 +4,16 @@ module RenderTemplate class WithoutLayoutController < ActionController::Base self.view_paths = [ActionView::FixtureResolver.new( - "test/basic.html.erb" => "Hello from basic.html.erb", - "shared.html.erb" => "Elastica", - "locals.html.erb" => "The secret is <%= secret %>", - "xml_template.xml.builder" => "xml.html do\n xml.p 'Hello'\nend", - "with_raw.html.erb" => "Hello <%=raw '<strong>this is raw</strong>' %>", - "test/with_json.html.erb" => "<%= render :template => 'test/with_json.json' %>", - "test/with_json.json.erb" => "<%= render :template => 'test/final' %>", - "test/final.json.erb" => "{ final: json }", - "test/with_error.html.erb" => "<%= idontexist %>" + "test/basic.html.erb" => "Hello from basic.html.erb", + "shared.html.erb" => "Elastica", + "locals.html.erb" => "The secret is <%= secret %>", + "xml_template.xml.builder" => "xml.html do\n xml.p 'Hello'\nend", + "with_raw.html.erb" => "Hello <%=raw '<strong>this is raw</strong>' %>", + "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %>", + "test/with_json.html.erb" => "<%= render :template => 'test/with_json.json' %>", + "test/with_json.json.erb" => "<%= render :template => 'test/final' %>", + "test/final.json.erb" => "{ final: json }", + "test/with_error.html.erb" => "<%= idontexist %>" )] def index @@ -51,6 +52,10 @@ module RenderTemplate render :template => "with_raw" end + def with_implicit_raw + render :template => "with_implicit_raw" + end + def with_error render :template => "test/with_error" end @@ -99,6 +104,11 @@ module RenderTemplate assert_body "Hello <strong>this is raw</strong>" assert_status 200 + + get :with_implicit_raw + + assert_body "Hello <strong>this is also raw</strong>" + assert_status 200 end test "rendering a template with renders another template with other format that renders other template in the same format" do diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index ecfa13d4ba..cd067b7d18 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -834,6 +834,14 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10')) end + def test_route_constraints_on_request_object_with_anchors_are_valid + assert_nothing_raised do + set.draw do + match 'page/:id' => 'pages#show', :constraints => { :host => /^foo$/ } + end + end + end + def test_route_constraints_with_anchor_chars_are_invalid assert_raise ArgumentError do set.draw do @@ -1436,7 +1444,7 @@ class RouteSetTest < ActiveSupport::TestCase end def test_expand_array_build_query_string - assert_uri_equal '/foo?x[]=1&x[]=2', default_route_set.generate({:controller => 'foo', :x => [1, 2]}) + assert_uri_equal '/foo?x%5B%5D=1&x%5B%5D=2', default_route_set.generate({:controller => 'foo', :x => [1, 2]}) end def test_escape_spaces_build_query_string_selected_keys @@ -1736,9 +1744,9 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert_equal '/posts', @routes.generate({:controller => 'posts'}, {:controller => 'posts', :action => 'index'}) assert_equal '/posts/create', @routes.generate({:action => 'create'}, {:controller => 'posts'}) assert_equal '/posts?foo=bar', @routes.generate(:controller => 'posts', :foo => 'bar') - assert_equal '/posts?foo[]=bar&foo[]=baz', @routes.generate(:controller => 'posts', :foo => ['bar', 'baz']) + assert_equal '/posts?foo%5B%5D=bar&foo%5B%5D=baz', @routes.generate(:controller => 'posts', :foo => ['bar', 'baz']) assert_equal '/posts?page=2', @routes.generate(:controller => 'posts', :page => 2) - assert_equal '/posts?q[foo][a]=b', @routes.generate(:controller => 'posts', :q => { :foo => { :a => 'b'}}) + assert_equal '/posts?q%5Bfoo%5D%5Ba%5D=b', @routes.generate(:controller => 'posts', :q => { :foo => { :a => 'b'}}) assert_equal '/news.rss', @routes.generate(:controller => 'news', :action => 'index', :format => 'rss') diff --git a/actionpack/test/controller/runner_test.rb b/actionpack/test/controller/runner_test.rb new file mode 100644 index 0000000000..24c220dcd5 --- /dev/null +++ b/actionpack/test/controller/runner_test.rb @@ -0,0 +1,22 @@ +require 'abstract_unit' +require 'action_dispatch/testing/integration' + +module ActionDispatch + class RunnerTest < Test::Unit::TestCase + class MyRunner + include Integration::Runner + + def initialize(session) + @integration_session = session + end + + def hi; end + end + + def test_respond_to? + runner = MyRunner.new(Class.new { def x; end }.new) + assert runner.respond_to?(:hi) + assert runner.respond_to?(:x) + end + end +end diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 4c07ca4cc3..1f62d29e80 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -17,7 +17,7 @@ module AbstractController end def test_exception_is_thrown_without_host - assert_raise RuntimeError do + assert_raise ArgumentError do W.new.url_for :controller => 'c', :action => 'a', :id => 'i' end end @@ -60,6 +60,27 @@ module AbstractController ) end + def test_subdomain_may_be_changed + add_host! + assert_equal('http://api.basecamphq.com/c/a/i', + W.new.url_for(:subdomain => 'api', :controller => 'c', :action => 'a', :id => 'i') + ) + end + + def test_domain_may_be_changed + add_host! + assert_equal('http://www.37signals.com/c/a/i', + W.new.url_for(:domain => '37signals.com', :controller => 'c', :action => 'a', :id => 'i') + ) + end + + def test_tld_length_may_be_changed + add_host! + assert_equal('http://mobile.www.basecamphq.com/c/a/i', + W.new.url_for(:subdomain => 'mobile', :tld_length => 2, :controller => 'c', :action => 'a', :id => 'i') + ) + end + def test_port add_host! assert_equal('http://www.basecamphq.com:3000/c/a/i', diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index efdc1f5d93..5ec7f12cc1 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -94,6 +94,16 @@ class CookiesTest < ActionController::TestCase cookies.delete(:user_name, :domain => :all) head :ok end + + def symbol_key + cookies[:user_name] = "david" + head :ok + end + + def string_key + cookies['user_name'] = "david" + head :ok + end end tests TestController @@ -135,11 +145,25 @@ class CookiesTest < ActionController::TestCase end def test_setting_cookie_with_secure + @request.env["HTTPS"] = "on" get :authenticate_with_secure assert_cookie_header "user_name=david; path=/; secure" assert_equal({"user_name" => "david"}, @response.cookies) end + def test_setting_cookie_with_secure_in_development + Rails.env.stubs(:development?).returns(true) + get :authenticate_with_secure + assert_cookie_header "user_name=david; path=/; secure" + assert_equal({"user_name" => "david"}, @response.cookies) + end + + def test_not_setting_cookie_with_secure + get :authenticate_with_secure + assert_not_cookie_header "user_name=david; path=/; secure" + assert_not_equal({"user_name" => "david"}, @response.cookies) + end + def test_multiple_cookies get :set_multiple_cookies assert_equal 2, @response.cookies.size @@ -277,6 +301,14 @@ class CookiesTest < ActionController::TestCase assert_cookie_header "user_name=; domain=.nextangle.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" end + def test_cookies_hash_is_indifferent_access + [:symbol_key, :string_key].each do |cookie_key| + get cookie_key + assert_equal "david", cookies[:user_name] + assert_equal "david", cookies['user_name'] + end + end + private def assert_cookie_header(expected) header = @response.headers["Set-Cookie"] @@ -286,4 +318,13 @@ class CookiesTest < ActionController::TestCase assert_equal expected.split("\n"), header end end + + def assert_not_cookie_header(expected) + header = @response.headers["Set-Cookie"] + if header.respond_to?(:to_str) + assert_not_equal expected.split("\n").sort, header.split("\n").sort + else + assert_not_equal expected.split("\n"), header + end + end end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 4c2b95550c..9424d88498 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -6,10 +6,65 @@ class MimeTypeTest < ActiveSupport::TestCase test "parse single" do Mime::LOOKUP.keys.each do |mime_type| - assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type) + unless mime_type == 'image/*' + assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type) + end end end + test "unregister" do + begin + Mime::Type.register("text/x-mobile", :mobile) + assert defined?(Mime::MOBILE) + assert_equal Mime::MOBILE, Mime::LOOKUP['text/x-mobile'] + assert_equal Mime::MOBILE, Mime::EXTENSION_LOOKUP['mobile'] + + Mime::Type.unregister("text/x-mobile", :mobile) + assert !defined?(Mime::MOBILE), "Mime::MOBILE should not be defined" + assert !Mime::LOOKUP.has_key?('text/x-mobile'), "Mime::LOOKUP should not have key ['text/x-mobile]" + assert !Mime::EXTENSION_LOOKUP.has_key?('mobile'), "Mime::EXTENSION_LOOKUP should not have key ['mobile]" + ensure + Mime.module_eval { remove_const :MOBILE if const_defined?(:MOBILE) } + Mime::LOOKUP.reject!{|key,_| key == 'text/x-mobile'} + end + end + + test "parse text with trailing star at the beginning" do + accept = "text/*, text/html, application/json, multipart/form-data" + expect = [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::TEXT, Mime::YAML, Mime::JS, Mime::MULTIPART_FORM] + parsed = Mime::Type.parse(accept) + assert_equal expect.size, parsed.size + Range.new(0,expect.size-1).to_a.each do |index| + assert_equal expect[index], parsed[index], "Failed for index number #{index}" + end + end + + test "parse text with trailing star in the end" do + accept = "text/html, application/json, multipart/form-data, text/*" + expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::XML, Mime::ICS, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT] + parsed = Mime::Type.parse(accept) + assert_equal 10, parsed.size + Range.new(0,expect.size-1).to_a.each do |index| + assert_equal expect[index], parsed[index], "Failed for index number #{index}" + end + end + + test "parse text with trailing star" do + accept = "text/*" + expect = [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT].sort_by(&:to_s) + parsed = Mime::Type.parse(accept) + assert_equal 9, parsed.size + assert_equal expect, parsed.sort_by(&:to_s) + end + + test "parse application with trailing star" do + accept = "application/*" + expect = [Mime::HTML, Mime::JS, Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::PDF, Mime::URL_ENCODED_FORM].sort_by(&:to_s) + parsed = Mime::Type.parse(accept) + assert_equal 9, parsed.size + assert_equal expect, parsed.sort_by(&:to_s) + end + test "parse without q" do accept = "text/xml,application/xhtml+xml,text/yaml,application/xml,text/html,image/png,text/plain,application/pdf,*/*" expect = [Mime::HTML, Mime::XML, Mime::YAML, Mime::PNG, Mime::TEXT, Mime::PDF, Mime::ALL] @@ -44,7 +99,7 @@ class MimeTypeTest < ActiveSupport::TestCase assert_equal Mime::GIF, Mime::SET.last end ensure - Mime.module_eval { remove_const :GIF if const_defined?(:GIF) } + Mime::Type.unregister('image/gif', :gif) end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 04709e5346..8f672c1149 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -96,6 +96,9 @@ class RequestTest < ActiveSupport::TestCase request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" assert_equal "rubyonrails.co.uk", request.domain(2) + request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk", :tld_length => 2 + assert_equal "rubyonrails.co.uk", request.domain + request = stub_request 'HTTP_HOST' => "192.168.1.200" assert_nil request.domain @@ -161,12 +164,20 @@ class RequestTest < ActiveSupport::TestCase assert !request.standard_port? end + test "optional port" do + request = stub_request 'HTTP_HOST' => 'www.example.org:80' + assert_equal nil, request.optional_port + + request = stub_request 'HTTP_HOST' => 'www.example.org:8080' + assert_equal 8080, request.optional_port + end + test "port string" do request = stub_request 'HTTP_HOST' => 'www.example.org:80' - assert_equal "", request.port_string + assert_equal '', request.port_string request = stub_request 'HTTP_HOST' => 'www.example.org:8080' - assert_equal ":8080", request.port_string + assert_equal ':8080', request.port_string end test "full path" do @@ -389,7 +400,7 @@ class RequestTest < ActiveSupport::TestCase mock_rack_env = { "QUERY_STRING" => "x[y]=1&x[y][][w]=2", "rack.input" => "foo" } request = nil begin - request = stub_request(mock_rack_env) + request = stub_request(mock_rack_env) request.parameters rescue TypeError => e # rack will raise a TypeError when parsing this query string diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 5c188a60c7..bbd010ea6d 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -155,6 +155,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end resources :replies do + collection do + get 'page/:page' => 'replies#index', :page => %r{\d+} + get ':page' => 'replies#index', :page => %r{\d+} + end + new do post :preview end @@ -1241,6 +1246,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper + with_test_routes do + assert_equal '/replies', replies_path + end + end + def test_scoped_controller_with_namespace_and_action with_test_routes do assert_equal '/account/twitter/callback', account_callback_path("twitter") @@ -2255,3 +2266,34 @@ class TestDefaultScope < ActionDispatch::IntegrationTest end end +class TestHttpMethods < ActionDispatch::IntegrationTest + RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) + RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) + RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY) + RFC3648 = %w(ORDERPATCH) + RFC3744 = %w(ACL) + RFC5323 = %w(SEARCH) + RFC5789 = %w(PATCH) + + def simple_app(response) + lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [response] ] } + end + + setup do + s = self + @app = ActionDispatch::Routing::RouteSet.new + + @app.draw do + (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method| + match '/' => s.simple_app(method), :via => method.underscore.to_sym + end + end + end + + (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method| + test "request method #{method.underscore} can be matched" do + get '/', nil, 'REQUEST_METHOD' => method + assert_equal method, @response.body + end + end +end diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index b51697b930..e2a7f1bad7 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -28,6 +28,18 @@ module ActionDispatch assert_equal 'foo', uf.tempfile end + def test_delegates_path_to_tempfile + tf = Class.new { def path; 'thunderhorse' end } + uf = Http::UploadedFile.new(:tempfile => tf.new) + assert_equal 'thunderhorse', uf.path + end + + def test_delegates_open_to_tempfile + tf = Class.new { def open; 'thunderhorse' end } + uf = Http::UploadedFile.new(:tempfile => tf.new) + assert_equal 'thunderhorse', uf.open + end + def test_delegates_to_tempfile tf = Class.new { def read; 'thunderhorse' end } uf = Http::UploadedFile.new(:tempfile => tf.new) diff --git a/actionpack/test/fixtures/layouts/_partial_and_yield.erb b/actionpack/test/fixtures/layouts/_partial_and_yield.erb new file mode 100644 index 0000000000..74cc428ffa --- /dev/null +++ b/actionpack/test/fixtures/layouts/_partial_and_yield.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'test/partial' %> +<%= yield %> diff --git a/actionpack/test/fixtures/layouts/_yield_only.erb b/actionpack/test/fixtures/layouts/_yield_only.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/actionpack/test/fixtures/layouts/_yield_only.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/actionpack/test/fixtures/layouts/_yield_with_params.erb b/actionpack/test/fixtures/layouts/_yield_with_params.erb new file mode 100644 index 0000000000..68e6557fb8 --- /dev/null +++ b/actionpack/test/fixtures/layouts/_yield_with_params.erb @@ -0,0 +1 @@ +<%= yield 'Yield!' %> diff --git a/actionpack/test/fixtures/layouts/yield_with_render_partial_inside.erb b/actionpack/test/fixtures/layouts/yield_with_render_partial_inside.erb new file mode 100644 index 0000000000..74cc428ffa --- /dev/null +++ b/actionpack/test/fixtures/layouts/yield_with_render_partial_inside.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'test/partial' %> +<%= yield %> diff --git a/actionpack/test/fixtures/star_star_mime/index.js.erb b/actionpack/test/fixtures/star_star_mime/index.js.erb new file mode 100644 index 0000000000..4da4181f56 --- /dev/null +++ b/actionpack/test/fixtures/star_star_mime/index.js.erb @@ -0,0 +1 @@ +function addition(a,b){ return a+b; } diff --git a/actionpack/test/fixtures/test/_partial_with_layout.erb b/actionpack/test/fixtures/test/_partial_with_layout.erb new file mode 100644 index 0000000000..2a50c834fe --- /dev/null +++ b/actionpack/test/fixtures/test/_partial_with_layout.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'test/partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Bar!' } %> +partial with layout diff --git a/actionpack/test/fixtures/test/_partial_with_layout_block_content.erb b/actionpack/test/fixtures/test/_partial_with_layout_block_content.erb new file mode 100644 index 0000000000..65dafd93a8 --- /dev/null +++ b/actionpack/test/fixtures/test/_partial_with_layout_block_content.erb @@ -0,0 +1,4 @@ +<%= render :layout => 'test/layout_for_partial', :locals => { :name => 'Bar!' } do %> + Content from inside layout! +<% end %> +partial with layout diff --git a/actionpack/test/fixtures/test/_partial_with_layout_block_partial.erb b/actionpack/test/fixtures/test/_partial_with_layout_block_partial.erb new file mode 100644 index 0000000000..444197a7d0 --- /dev/null +++ b/actionpack/test/fixtures/test/_partial_with_layout_block_partial.erb @@ -0,0 +1,4 @@ +<%= render :layout => 'test/layout_for_partial', :locals => { :name => 'Bar!' } do %> + <%= render 'test/partial' %> +<% end %> +partial with layout diff --git a/actionpack/test/fixtures/test/_partial_with_partial.erb b/actionpack/test/fixtures/test/_partial_with_partial.erb new file mode 100644 index 0000000000..ee0d5037b6 --- /dev/null +++ b/actionpack/test/fixtures/test/_partial_with_partial.erb @@ -0,0 +1,2 @@ +<%= render 'test/partial' %> +partial with partial diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb index dba632e6df..bd18cdc1b8 100644 --- a/actionpack/test/lib/controller/fake_models.rb +++ b/actionpack/test/lib/controller/fake_models.rb @@ -184,3 +184,19 @@ module Blog end end end + +class ArelLike + def to_ary + true + end + def each + a = Array.new(2) { |id| Comment.new(id + 1) } + a.each { |i| yield i } + end +end + +class RenderJsonTestException < Exception + def to_json(options = nil) + return { :error => self.class.name, :message => self.to_str }.to_json + end +end diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 3abcdfbc1e..fbcc99a17a 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -274,7 +274,7 @@ class AssetTagHelperTest < ActionView::TestCase end def test_reset_javascript_expansions - ActionView::Helpers::AssetTagHelper.javascript_expansions.clear + JavascriptIncludeTag.expansions.clear assert_raise(ArgumentError) { javascript_include_tag(:defaults) } end @@ -306,7 +306,6 @@ class AssetTagHelperTest < ActionView::TestCase ENV["RAILS_ASSET_ID"] = "" assert stylesheet_link_tag('dir/file').html_safe? assert stylesheet_link_tag('dir/other/file', 'dir/file2').html_safe? - assert stylesheet_tag('dir/file', {}).html_safe? end def test_custom_stylesheet_expansions @@ -681,6 +680,26 @@ class AssetTagHelperTest < ActionView::TestCase FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) end + def test_caching_javascript_include_tag_with_named_paths_and_relative_url_root_when_caching_off + ENV["RAILS_ASSET_ID"] = "" + @controller.config.relative_url_root = "/collaboration/hieraki" + config.perform_caching = false + + assert_dom_equal( + %(<script src="/collaboration/hieraki/javascripts/robber.js" type="text/javascript"></script>), + javascript_include_tag('robber', :cache => true) + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) + + assert_dom_equal( + %(<script src="/collaboration/hieraki/javascripts/robber.js" type="text/javascript"></script>), + javascript_include_tag('robber', :cache => "money", :recursive => true) + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) + end + def test_caching_javascript_include_tag_when_caching_off ENV["RAILS_ASSET_ID"] = "" config.perform_caching = false @@ -727,6 +746,17 @@ class AssetTagHelperTest < ActionView::TestCase assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) end + def test_caching_javascript_include_tag_when_caching_on_and_javascript_file_is_uri + ENV["RAILS_ASSET_ID"] = "" + config.perform_caching = true + + assert_raise(Errno::ENOENT) { + javascript_include_tag('bank', 'robber', 'https://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.js', :cache => true) + } + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) + end + def test_caching_javascript_include_tag_when_caching_off_and_missing_javascript_file ENV["RAILS_ASSET_ID"] = "" config.perform_caching = false @@ -897,6 +927,30 @@ class AssetTagHelperTest < ActionView::TestCase FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) end + + def test_caching_stylesheet_link_tag_with_named_paths_and_relative_url_root_when_caching_off + ENV["RAILS_ASSET_ID"] = "" + @controller.config.relative_url_root = "/collaboration/hieraki" + config.perform_caching = false + + assert_dom_equal( + %(<link href="/collaboration/hieraki/stylesheets/robber.css" media="screen" rel="stylesheet" type="text/css" />), + stylesheet_link_tag('robber', :cache => true) + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) + + assert_dom_equal( + %(<link href="/collaboration/hieraki/stylesheets/robber.css" media="screen" rel="stylesheet" type="text/css" />), + stylesheet_link_tag('robber', :cache => "money") + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) + end + + + + def test_caching_stylesheet_include_tag_when_caching_off ENV["RAILS_ASSET_ID"] = "" config.perform_caching = false @@ -938,7 +992,7 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase @request = Struct.new(:protocol).new("gopher://") @controller.request = @request - ActionView::Helpers::AssetTagHelper.javascript_expansions.clear + JavascriptIncludeTag.expansions.clear end def url_for(options) diff --git a/actionpack/test/template/capture_helper_test.rb b/actionpack/test/template/capture_helper_test.rb index 8f81076299..03050485fa 100644 --- a/actionpack/test/template/capture_helper_test.rb +++ b/actionpack/test/template/capture_helper_test.rb @@ -28,6 +28,16 @@ class CaptureHelperTest < ActionView::TestCase assert_nil @av.capture { 1 } end + def test_capture_escapes_html + string = @av.capture { '<em>bar</em>' } + assert_equal '<em>bar</em>', string + end + + def test_capture_doesnt_escape_twice + string = @av.capture { '<em>bar</em>'.html_safe } + assert_equal '<em>bar</em>', string + end + def test_content_for assert ! content_for?(:title) content_for :title, 'title' diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index 0cf7885772..55c384e68f 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -1584,6 +1584,47 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on", { :date_separator => " / " }) end + def test_date_select_with_separator_and_order + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :date_separator => " / " }) + end + + def test_date_select_with_separator_and_order_and_year_discarded + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + expected << %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + + assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :discard_year => true, :date_separator => " / " }) + end + def test_date_select_with_default_prompt @post = Post.new @post.written_on = Date.new(2004, 6, 15) diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index acb6e7aa64..2c60096475 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -1230,6 +1230,27 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_nested_fields_for_arel_like + @post.comments = ArelLike.new + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, @post.comments) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') 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 + def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection_different_from_record_one comments = Array.new(2) { |id| Comment.new(id + 1) } @post.comments = [] diff --git a/actionpack/test/template/lookup_context_test.rb b/actionpack/test/template/lookup_context_test.rb index 6d3b26e131..c9dd27cf2a 100644 --- a/actionpack/test/template/lookup_context_test.rb +++ b/actionpack/test/template/lookup_context_test.rb @@ -180,6 +180,16 @@ class LookupContextTest < ActiveSupport::TestCase assert_not_equal template, old_template end + + test "data can be stored in cached templates" do + template = @lookup_context.find("hello_world", "test") + template.data["cached"] = "data" + assert_equal "Hello world!", template.source + + template = @lookup_context.find("hello_world", "test") + assert_equal "data", template.data["cached"] + assert_equal "Hello world!", template.source + end end class LookupContextWithFalseCaching < ActiveSupport::TestCase @@ -205,7 +215,7 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase assert_equal "Bar", template.source end - test "if no template was found in the second lookup, give it higher preference" do + test "if no template was found in the second lookup, with no cache, raise error" do template = @lookup_context.find("foo", "test", true) assert_equal "Foo", template.source @@ -215,7 +225,7 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase end end - test "if no template was cached in the first lookup, do not use the cache in the second" do + test "if no template was cached in the first lookup, retrieval should work in the second call" do @resolver.hash.clear assert_raise ActionView::MissingTemplate do @lookup_context.find("foo", "test", true) @@ -225,4 +235,19 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase template = @lookup_context.find("foo", "test", true) assert_equal "Foo", template.source end + + test "data can be stored as long as template was not updated" do + template = @lookup_context.find("foo", "test", true) + template.data["cached"] = "data" + assert_equal "Foo", template.source + + template = @lookup_context.find("foo", "test", true) + assert_equal "data", template.data["cached"] + assert_equal "Foo", template.source + + @resolver.hash["test/_foo.erb"][1] = Time.now.utc + template = @lookup_context.find("foo", "test", true) + assert_nil template.data["cached"] + assert_equal "Foo", template.source + end end
\ No newline at end of file diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index c82ead663f..5df09b4d3b 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -7,7 +7,7 @@ class NumberHelperTest < ActionView::TestCase I18n.backend.store_translations 'ts', :number => { :format => { :precision => 3, :delimiter => ',', :separator => '.', :significant => false, :strip_insignificant_zeros => false }, - :currency => { :format => { :unit => '&$', :format => '%u - %n', :precision => 2 } }, + :currency => { :format => { :unit => '&$', :format => '%u - %n', :negative_format => '(%u - %n)', :precision => 2 } }, :human => { :format => { :precision => 2, @@ -43,11 +43,14 @@ class NumberHelperTest < ActionView::TestCase def test_number_to_i18n_currency assert_equal("&$ - 10.00", number_to_currency(10, :locale => 'ts')) + assert_equal("(&$ - 10.00)", number_to_currency(-10, :locale => 'ts')) + assert_equal("-10.00 - &$", number_to_currency(-10, :locale => 'ts', :format => "%n - %u")) end def test_number_to_currency_with_clean_i18n_settings clean_i18n do assert_equal("$10.00", number_to_currency(10)) + assert_equal("-$10.00", number_to_currency(-10)) end end diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb index dcdf28ddd5..ab127521ad 100644 --- a/actionpack/test/template/number_helper_test.rb +++ b/actionpack/test/template/number_helper_test.rb @@ -45,11 +45,15 @@ class NumberHelperTest < ActionView::TestCase def test_number_to_currency assert_equal("$1,234,567,890.50", number_to_currency(1234567890.50)) assert_equal("$1,234,567,890.51", number_to_currency(1234567890.506)) + assert_equal("-$1,234,567,890.50", number_to_currency(-1234567890.50)) + assert_equal("-$ 1,234,567,890.50", number_to_currency(-1234567890.50, {:format => "%u %n"})) + assert_equal("($1,234,567,890.50)", number_to_currency(-1234567890.50, {:negative_format => "(%u%n)"})) assert_equal("$1,234,567,892", number_to_currency(1234567891.50, {:precision => 0})) assert_equal("$1,234,567,890.5", number_to_currency(1234567890.50, {:precision => 1})) assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""})) assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50")) assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) + assert_equal("1,234,567,890.50 - Kč", number_to_currency("-1234567890.50", {:unit => "Kč", :format => "%n %u", :negative_format => "%n - %u"})) end def test_number_to_percentage diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index 17bb610b6a..8087429d62 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -221,6 +221,15 @@ module RenderTestCases "@output_buffer << 'source: #{template.source.inspect}'\n" end + WithViewHandler = lambda do |template, view| + %'"#{template.class} #{view.class}"' + end + + def test_render_inline_with_template_handler_with_view + ActionView::Template.register_template_handler :with_view, WithViewHandler + assert_equal 'ActionView::Template ActionView::Base', @view.render(:inline => "Hello, World!", :type => :with_view) + end + def test_render_inline_with_compilable_custom_type ActionView::Template.register_template_handler :foo, CustomHandler assert_equal 'source: "Hello, World!"', @view.render(:inline => "Hello, World!", :type => :foo) @@ -247,6 +256,51 @@ module RenderTestCases @view.render(:file => "test/hello_world.erb", :layout => "layouts/yield_with_render_inline_inside") end + def test_render_with_layout_which_renders_another_partial + assert_equal %(partial html\nHello world!\n), + @view.render(:file => "test/hello_world.erb", :layout => "layouts/yield_with_render_partial_inside") + end + + def test_render_layout_with_block_and_yield + assert_equal %(Content from block!\n), + @view.render(:layout => "layouts/yield_only") { "Content from block!" } + end + + def test_render_layout_with_block_and_yield_with_params + assert_equal %(Yield! Content from block!\n), + @view.render(:layout => "layouts/yield_with_params") { |param| "#{param} Content from block!" } + end + + def test_render_layout_with_block_which_renders_another_partial_and_yields + assert_equal %(partial html\nContent from block!\n), + @view.render(:layout => "layouts/partial_and_yield") { "Content from block!" } + end + + def test_render_partial_and_layout_without_block_with_locals + assert_equal %(Before (Foo!)\npartial html\nAfter), + @view.render(:partial => 'test/partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_partial_and_layout_without_block_with_locals_and_rendering_another_partial + assert_equal %(Before (Foo!)\npartial html\npartial with partial\n\nAfter), + @view.render(:partial => 'test/partial_with_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_layout_with_a_nested_render_layout_call + assert_equal %(Before (Foo!)\nBefore (Bar!)\npartial html\nAfter\npartial with layout\n\nAfter), + @view.render(:partial => 'test/partial_with_layout', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_partial + assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n partial html\n\nAfterpartial with layout\n\nAfter), + @view.render(:partial => 'test/partial_with_layout_block_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_content + assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n Content from inside layout!\n\nAfterpartial with layout\n\nAfter), + @view.render(:partial => 'test/partial_with_layout_block_content', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + def test_render_with_nested_layout assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n), @view.render(:file => "test/nested_layout.erb", :layout => "layouts/yield") diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index a6fdf806a4..4a8cea36d4 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -263,12 +263,7 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal "<strong>Showing</strong>", link_to_unless(true, "Showing", url_hash) { |name| - "<strong>#{name}</strong>" - } - - assert_equal "<strong>Showing</strong>", - link_to_unless(true, "Showing", url_hash) { |name| - "<strong>#{name}</strong>" + "<strong>#{name}</strong>".html_safe } assert_equal "test", @@ -433,6 +428,10 @@ class UrlHelperControllerTest < ActionController::TestCase match 'url_helper_controller_test/url_helper/normalize_recall_params', :to => UrlHelperController.action(:normalize_recall), :as => :normalize_recall_params + + match '/url_helper_controller_test/url_helper/override_url_helper/default', + :to => 'url_helper_controller_test/url_helper#override_url_helper', + :as => :override_url_helper end def show @@ -468,6 +467,15 @@ class UrlHelperControllerTest < ActionController::TestCase end def rescue_action(e) raise e end + + def override_url_helper + render :inline => '<%= override_url_helper_path %>' + end + + def override_url_helper_path + '/url_helper_controller_test/url_helper/override_url_helper/override' + end + helper_method :override_url_helper_path end tests UrlHelperController @@ -527,6 +535,11 @@ class UrlHelperControllerTest < ActionController::TestCase get :show, :name => '123' assert_equal 'ok', @response.body end + + def test_url_helper_can_be_overriden + get :override_url_helper + assert_equal '/url_helper_controller_test/url_helper/override_url_helper/override', @response.body + end end class TasksController < ActionController::Base diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index e93d84ab59..4e963c77b0 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -2,6 +2,10 @@ * No changes +*Rails 3.0.2 (unreleased)* + +* No changes + *Rails 3.0.1 (October 15, 2010)* * No Changes, just a version bump. diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 0372c7a03e..0372c7a03e 100644..100755 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index c483ecbc3c..318d71a610 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -20,6 +20,6 @@ Gem::Specification.new do |s| s.has_rdoc = true s.add_dependency('activesupport', version) - s.add_dependency('builder', '~> 2.1.2') - s.add_dependency('i18n', '~> 0.4.1') + s.add_dependency('builder', '~> 3.0.0') + s.add_dependency('i18n', '~> 0.4.2') end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 44e3e64a9e..fc5f5c4c66 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -46,8 +46,8 @@ module ActiveModel # end # end # - # Notice that whenever you include ActiveModel::AttributeMethods in your class, - # it requires you to implement a <tt>attributes</tt> methods which returns a hash + # Note that whenever you include ActiveModel::AttributeMethods in your class, + # it requires you to implement an <tt>attributes</tt> method which returns a hash # with each attribute name in your model as hash key and the attribute value as # hash value. # @@ -98,10 +98,10 @@ module ActiveModel def define_attr_method(name, value=nil, &block) sing = singleton_class sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 - if method_defined?(:original_#{name}) - undef :original_#{name} + if method_defined?(:'original_#{name}') + undef :'original_#{name}' end - alias_method :original_#{name}, :#{name} + alias_method :'original_#{name}', :'#{name}' eorb if block_given? sing.send :define_method, name, &block @@ -274,8 +274,8 @@ module ActiveModel method_name = matcher.method_name(attr_name) generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - if method_defined?(:#{method_name}) - undef :#{method_name} + if method_defined?(:'#{method_name}') + undef :'#{method_name}' end def #{method_name}(*args) send(:#{matcher.method_missing_target}, '#{attr_name}', *args) diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index adb71f788f..44aedc8efd 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,5 +1,6 @@ require 'active_support/inflector' require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/module/introspection' module ActiveModel class Name < String diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index ece4431225..23ba42bf35 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -3,8 +3,8 @@ module ActiveModel MAJOR = 3 MINOR = 1 TINY = 0 - BUILD = "beta" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index a81584bbad..01f0158678 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -12,9 +12,3 @@ ActiveSupport::Deprecation.debug = true require 'rubygems' require 'test/unit' - -begin - require 'ruby-debug' - Debugger.start -rescue LoadError -end diff --git a/activemodel/test/cases/serializeration/xml_serialization_test.rb b/activemodel/test/cases/serializeration/xml_serialization_test.rb index ff786658b7..cc19d322b3 100644 --- a/activemodel/test/cases/serializeration/xml_serialization_test.rb +++ b/activemodel/test/cases/serializeration/xml_serialization_test.rb @@ -70,6 +70,13 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<CreatedAt}, @xml end + test "should allow lower-camelized tags" do + @xml = @contact.to_xml :root => 'xml_contact', :camelize => :lower + assert_match %r{^<xmlContact>}, @xml + assert_match %r{</xmlContact>$}, @xml + assert_match %r{<createdAt}, @xml + end + test "should allow skipped types" do @xml = @contact.to_xml :skip_types => true assert_match %r{<age>25</age>}, @xml diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index d3530e0da9..f46db909ba 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,43 @@ *Rails 3.1.0 (unreleased)* +* ActiveRecord::Base#dup and ActiveRecord::Base#clone semantics have changed +to closer match normal Ruby dup and clone semantics. + +* Calling ActiveRecord::Base#clone will result in a shallow copy of the record, +including copying the frozen state. No callbacks will be called. + +* Calling ActiveRecord::Base#dup will duplicate the record, including calling +after initialize hooks. Frozen state will not be copied, and all associations +will be cleared. A duped record will return true for new_record?, have a nil +id field, and is saveable. + +* Migrations can be defined as reversible, meaning that the migration system +will figure out how to reverse your migration. To use reversible migrations, +just define the "change" method. For example: + + class MyMigration < ActiveRecord::Migration + def change + create_table(:horses) do + t.column :content, :text + t.column :remind_at, :datetime + end + end + end + +Some things cannot be automatically reversed for you. If you know how to +reverse those things, you should define 'up' and 'down' in your migration. If +you define something in `change` that cannot be reversed, an +IrreversibleMigration exception will be raised when going down. + +* Migrations should use instance methods rather than class methods: + class FooMigration < ActiveRecord::Migration + def up + ... + end + end + + [Aaron Patterson] + * has_one maintains the association with separate after_create/after_update instead of a single after_save. [fxn] @@ -18,6 +56,35 @@ [Aaron Patterson] +*Rails 3.0.2 (unreleased)* + +* reorder is deprecated in favor of except(:order).order(...) [Santiago Pastorino] + +* except is now AR public API + + Model.order('name').except(:order).order('salary') + + generates: + + SELECT * FROM models ORDER BY salary + + [Santiago Pastorino] + +* The following code: + + Model.limit(10).scoping { Model.count } + + now generates the following SQL: + + SELECT COUNT(*) FROM models LIMIT 10 + + This may not return what you want. Instead, you may with to do something + like this: + + Model.limit(10).scoping { Model.all.size } + + [Aaron Patterson] + *Rails 3.0.1 (October 15, 2010)* * Introduce a fix for CVE-2010-3993 diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 395c72dfbc..f9b77c1799 100644..100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rake/gempackagetask' diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index b8a980b1fb..b1df24844a 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -23,6 +23,6 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('arel', '~> 2.0.0') + s.add_dependency('arel', '~> 2.0.2') s.add_dependency('tzinfo', '~> 0.3.23') end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 16206c1056..8cd7389005 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -6,7 +6,7 @@ module ActiveRecord def clear_aggregation_cache #:nodoc: self.class.reflect_on_all_aggregations.to_a.each do |assoc| instance_variable_set "@#{assoc.name}", nil - end unless self.new_record? + end if self.persisted? end # Active Record implements aggregation through a macro-like class method called +composed_of+ diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index e6b367790b..5eb1071ba2 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -193,13 +193,17 @@ module ActiveRecord conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(reflection, preload_options) - associated_records = reflection.klass.unscoped.where([conditions, ids]). + associated_records_proxy = reflection.klass.unscoped. includes(options[:include]). joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}"). select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id"). - order(options[:order]).to_a + order(options[:order]) - set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id') + all_associated_records = associated_records(ids) do |some_ids| + associated_records_proxy.where([conditions, ids]).to_a + end + + set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id') end def preload_has_one_association(records, reflection, preload_options={}) @@ -256,9 +260,6 @@ module ActiveRecord end def preload_through_records(records, reflection, through_association) - through_reflection = reflections[through_association] - - through_records = [] if reflection.options[:source_type] interface = reflection.source_reflection.options[:foreign_type] preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} @@ -267,16 +268,15 @@ module ActiveRecord records.first.class.preload_associations(records, through_association, preload_options) # Dont cache the association - we would only be caching a subset - records.each do |record| + records.map { |record| proxy = record.send(through_association) if proxy.respond_to?(:target) - through_records.concat Array.wrap(proxy.target) - proxy.reset + Array.wrap(proxy.target).tap { proxy.reset } else # this is a has_one :through reflection - through_records << proxy if proxy + [proxy].compact end - end + }.flatten(1) else options = {} options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions] @@ -284,11 +284,10 @@ module ActiveRecord options[:conditions] = reflection.options[:conditions] records.first.class.preload_associations(records, through_association, options) - records.each do |record| - through_records.concat Array.wrap(record.send(through_association)) - end + records.map { |record| + Array.wrap(record.send(through_association)) + }.flatten(1) end - through_records end def preload_belongs_to_association(records, reflection, preload_options={}) @@ -325,7 +324,7 @@ module ActiveRecord klass = klass_name.constantize table_name = klass.quoted_table_name - primary_key = reflection.options[:primary_key] || klass.primary_key + primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s column_type = klass.columns.detect{|c| c.name == primary_key}.type ids = _id_map.keys.map do |id| @@ -363,13 +362,14 @@ module ActiveRecord find_options = { :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"), :include => preload_options[:include] || options[:include], - :conditions => [conditions, ids], :joins => options[:joins], :group => preload_options[:group] || options[:group], :order => preload_options[:order] || options[:order] } - reflection.klass.scoped.apply_finder_options(find_options).to_a + associated_records(ids) do |some_ids| + reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => [conditions, some_ids])).to_a + end end @@ -387,6 +387,17 @@ module ActiveRecord def in_or_equals_for_ids(ids) ids.size > 1 ? "IN (?)" : "= ?" end + + # Some databases impose a limit on the number of ids in a list (in Oracle its 1000) + # Make several smaller queries if necessary or make one query if the adapter supports it + def associated_records(ids) + in_clause_length = connection.in_clause_length || ids.size + records = [] + ids.each_slice(in_clause_length) do |some_ids| + records += yield(some_ids) + end + records + end end end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 59d328f207..0d9171d876 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -4,6 +4,8 @@ require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/class/attribute' +require 'active_record/associations/class_methods/join_dependency' module ActiveRecord class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: @@ -118,7 +120,7 @@ module ActiveRecord def clear_association_cache #:nodoc: self.class.reflect_on_all_associations.to_a.each do |assoc| instance_variable_set "@#{assoc.name}", nil - end unless self.new_record? + end if self.persisted? end private @@ -1810,12 +1812,12 @@ module ActiveRecord callbacks.each do |callback_name| full_callback_name = "#{callback_name}_for_#{association_name}" defined_callbacks = options[callback_name.to_sym] - if options.has_key?(callback_name.to_sym) - class_inheritable_reader full_callback_name.to_sym - write_inheritable_attribute(full_callback_name.to_sym, [defined_callbacks].flatten) - else - write_inheritable_attribute(full_callback_name.to_sym, []) - end + + full_callback_value = options.has_key?(callback_name.to_sym) ? [defined_callbacks].flatten : [] + + # TODO : why do i need method_defined? I think its because of the inheritance chain + class_attribute full_callback_name.to_sym unless method_defined?(full_callback_name) + self.send("#{full_callback_name}=", full_callback_value) end end @@ -1831,555 +1833,6 @@ module ActiveRecord Array.wrap(extensions) end end - - class JoinDependency # :nodoc: - attr_reader :join_parts, :reflections, :table_aliases - - def initialize(base, associations, joins) - @join_parts = [JoinBase.new(base, joins)] - @associations = associations - @reflections = [] - @base_records_hash = {} - @base_records_in_order = [] - @table_aliases = Hash.new(0) - @table_aliases[base.table_name] = 1 - build(associations) - end - - def graft(*associations) - associations.each do |association| - join_associations.detect {|a| association == a} || - build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type) - end - self - end - - def join_associations - join_parts.last(join_parts.length - 1) - end - - def join_base - join_parts.first - end - - def count_aliases_from_table_joins(name) - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase - join_sql = join_base.table_joins.to_s.downcase - join_sql.blank? ? 0 : - # Table names - join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size + - # Table aliases - join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size - end - - def instantiate(rows) - rows.each_with_index do |row, i| - primary_id = join_base.record_id(row) - unless @base_records_hash[primary_id] - @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row)) - end - construct(@base_records_hash[primary_id], @associations, join_associations.dup, row) - end - remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations) - return @base_records_in_order - end - - def remove_duplicate_results!(base, records, associations) - case associations - when Symbol, String - reflection = base.reflections[associations] - remove_uniq_by_reflection(reflection, records) - when Array - associations.each do |association| - remove_duplicate_results!(base, records, association) - end - when Hash - associations.keys.each do |name| - reflection = base.reflections[name] - remove_uniq_by_reflection(reflection, records) - - parent_records = [] - records.each do |record| - if descendant = record.send(reflection.name) - if reflection.collection? - parent_records.concat descendant.target.uniq - else - parent_records << descendant - end - end - end - - remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? - end - end - end - - protected - - def build(associations, parent = nil, join_type = Arel::InnerJoin) - parent ||= join_parts.last - case associations - when Symbol, String - reflection = parent.reflections[associations.to_s.intern] or - raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" - @reflections << reflection - join_association = build_join_association(reflection, parent) - join_association.join_type = join_type - @join_parts << join_association - when Array - associations.each do |association| - build(association, parent, join_type) - end - when Hash - associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - build(name, parent, join_type) - build(associations[name], nil, join_type) - end - else - raise ConfigurationError, associations.inspect - end - end - - def remove_uniq_by_reflection(reflection, records) - if reflection && reflection.collection? - records.each { |record| record.send(reflection.name).target.uniq! } - end - end - - def build_join_association(reflection, parent) - JoinAssociation.new(reflection, self, parent) - end - - def construct(parent, associations, join_parts, row) - case associations - when Symbol, String - join_part = join_parts.detect { |j| - j.reflection.name.to_s == associations.to_s && - j.parent_table_name == parent.class.table_name } - raise(ConfigurationError, "No such association") if join_part.nil? - - join_parts.delete(join_part) - construct_association(parent, join_part, row) - when Array - associations.each do |association| - construct(parent, association, join_parts, row) - end - when Hash - associations.sort_by { |k,_| k.to_s }.each do |name, assoc| - join_part = join_parts.detect{ |j| - j.reflection.name.to_s == name.to_s && - j.parent_table_name == parent.class.table_name } - raise(ConfigurationError, "No such association") if join_part.nil? - - association = construct_association(parent, join_part, row) - join_parts.delete(join_part) - construct(association, assoc, join_parts, row) if association - end - else - raise ConfigurationError, associations.inspect - end - end - - def construct_association(record, join_part, row) - return if record.id.to_s != join_part.parent.record_id(row).to_s - - macro = join_part.reflection.macro - if macro == :has_one - return if record.instance_variable_defined?("@#{join_part.reflection.name}") - association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? - set_target_and_inverse(join_part, association, record) - else - return if row[join_part.aliased_primary_key].nil? - association = join_part.instantiate(row) - case macro - when :has_many, :has_and_belongs_to_many - collection = record.send(join_part.reflection.name) - collection.loaded - collection.target.push(association) - collection.__send__(:set_inverse_instance, association, record) - when :belongs_to - set_target_and_inverse(join_part, association, record) - else - raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" - end - end - association - end - - def set_target_and_inverse(join_part, association, record) - association_proxy = record.send("set_#{join_part.reflection.name}_target", association) - association_proxy.__send__(:set_inverse_instance, association, record) - end - - # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited - # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which - # everything else is being joined onto. A JoinAssociation represents an association which - # is joining to the base. A JoinAssociation may result in more than one actual join - # operations (for example a has_and_belongs_to_many JoinAssociation would result in - # two; one for the join table and one for the target table). - class JoinPart # :nodoc: - # The Active Record class which this join part is associated 'about'; for a JoinBase - # this is the actual base model, for a JoinAssociation this is the target model of the - # association. - attr_reader :active_record - - delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record - - def initialize(active_record) - @active_record = active_record - @cached_record = {} - end - - def ==(other) - raise NotImplementedError - end - - # An Arel::Table for the active_record - def table - raise NotImplementedError - end - - # The prefix to be used when aliasing columns in the active_record's table - def aliased_prefix - raise NotImplementedError - end - - # The alias for the active_record's table - def aliased_table_name - raise NotImplementedError - end - - # The alias for the primary key of the active_record's table - def aliased_primary_key - "#{aliased_prefix}_r0" - end - - # An array of [column_name, alias] pairs for the table - def column_names_with_alias - unless defined?(@column_names_with_alias) - @column_names_with_alias = [] - - ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| - @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] - end - end - - @column_names_with_alias - end - - def extract_record(row) - Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}] - end - - def record_id(row) - row[aliased_primary_key] - end - - def instantiate(row) - @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row)) - end - end - - class JoinBase < JoinPart # :nodoc: - # Extra joins provided when the JoinDependency was created - attr_reader :table_joins - - def initialize(active_record, joins = nil) - super(active_record) - @table_joins = joins - end - - def ==(other) - other.class == self.class && - other.active_record == active_record && - other.table_joins == table_joins - end - - def aliased_prefix - "t0" - end - - def table - Arel::Table.new(table_name, :engine => arel_engine, :columns => active_record.columns) - end - - def aliased_table_name - active_record.table_name - end - end - - class JoinAssociation < JoinPart # :nodoc: - # The reflection of the association represented - attr_reader :reflection - - # The JoinDependency object which this JoinAssociation exists within. This is mainly - # relevant for generating aliases which do not conflict with other joins which are - # part of the query. - attr_reader :join_dependency - - # A JoinBase instance representing the active record we are joining onto. - # (So in Author.has_many :posts, the Author would be that base record.) - attr_reader :parent - - # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin - attr_accessor :join_type - - # These implement abstract methods from the superclass - attr_reader :aliased_prefix, :aliased_table_name - - delegate :options, :through_reflection, :source_reflection, :to => :reflection - delegate :table, :table_name, :to => :parent, :prefix => true - - def initialize(reflection, join_dependency, parent = nil) - reflection.check_validity! - - if reflection.options[:polymorphic] - raise EagerLoadPolymorphicError.new(reflection) - end - - super(reflection.klass) - - @reflection = reflection - @join_dependency = join_dependency - @parent = parent - @join_type = Arel::InnerJoin - - # This must be done eagerly upon initialisation because the alias which is produced - # depends on the state of the join dependency, but we want it to work the same way - # every time. - allocate_aliases - end - - def ==(other) - other.class == self.class && - other.reflection == reflection && - other.parent == parent - end - - def find_parent_in(other_join_dependency) - other_join_dependency.join_parts.detect do |join_part| - self.parent == join_part - end - end - - def join_to(relation) - send("join_#{reflection.macro}_to", relation) - end - - def join_relation(joining_relation) - self.join_type = Arel::OuterJoin - joining_relation.joins(self) - end - - def table - @table ||= Arel::Table.new( - table_name, :as => aliased_table_name, - :engine => arel_engine, :columns => active_record.columns - ) - end - - # More semantic name given we are talking about associations - alias_method :target_table, :table - - protected - - def aliased_table_name_for(name, suffix = nil) - if @join_dependency.table_aliases[name].zero? - @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name) - end - - if !@join_dependency.table_aliases[name].zero? # We need an alias - name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" - @join_dependency.table_aliases[name] += 1 - if @join_dependency.table_aliases[name] == 1 # First time we've seen this name - # Also need to count the aliases from the table_aliases to avoid incorrect count - @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name) - end - table_index = @join_dependency.table_aliases[name] - name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1 - else - @join_dependency.table_aliases[name] += 1 - end - - name - end - - def pluralize(table_name) - ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name - end - - def interpolate_sql(sql) - instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) - end - - private - - def allocate_aliases - @aliased_prefix = "t#{ join_dependency.join_parts.size }" - @aliased_table_name = aliased_table_name_for(table_name) - - if reflection.macro == :has_and_belongs_to_many - @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") - elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] - @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") - end - end - - def process_conditions(conditions, table_name) - Arel.sql(interpolate_sql(sanitize_sql(conditions, table_name))) - end - - def join_target_table(relation, *conditions) - relation = relation.join(target_table, join_type) - - # If the target table is an STI model then we must be sure to only include records of - # its type and its sub-types. - unless active_record.descends_from_active_record? - sti_column = target_table[active_record.inheritance_column] - - sti_condition = sti_column.eq(active_record.sti_name) - active_record.descendants.each do |subclass| - sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) - end - - conditions << sti_condition - end - - # If the reflection has conditions, add them - if options[:conditions] - conditions << process_conditions(options[:conditions], aliased_table_name) - end - - relation = relation.on(*conditions) - end - - def join_has_and_belongs_to_many_to(relation) - join_table = Arel::Table.new( - options[:join_table], :engine => arel_engine, - :as => @aliased_join_table_name - ) - - fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key - klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key - - relation = relation.join(join_table, join_type) - relation = relation.on( - join_table[fk]. - eq(parent_table[reflection.active_record.primary_key]) - ) - - join_target_table( - relation, - target_table[reflection.klass.primary_key]. - eq(join_table[klass_fk]) - ) - end - - def join_has_many_to(relation) - if reflection.options[:through] - join_has_many_through_to(relation) - elsif reflection.options[:as] - join_has_many_polymorphic_to(relation) - else - foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key - primary_key = options[:primary_key] || parent.primary_key - - join_target_table( - relation, - target_table[foreign_key]. - eq(parent_table[primary_key]) - ) - end - end - alias :join_has_one_to :join_has_many_to - - def join_has_many_through_to(relation) - join_table = Arel::Table.new( - through_reflection.klass.table_name, :engine => arel_engine, - :as => @aliased_join_table_name - ) - - jt_conditions = [] - jt_foreign_key = first_key = second_key = nil - - if through_reflection.options[:as] # has_many :through against a polymorphic join - as_key = through_reflection.options[:as].to_s - jt_foreign_key = as_key + '_id' - - jt_conditions << - join_table[as_key + '_type']. - eq(parent.active_record.base_class.name) - else - jt_foreign_key = through_reflection.primary_key_name - end - - case source_reflection.macro - when :has_many - second_key = options[:foreign_key] || primary_key - - if source_reflection.options[:as] - first_key = "#{source_reflection.options[:as]}_id" - else - first_key = through_reflection.klass.base_class.to_s.foreign_key - end - - unless through_reflection.klass.descends_from_active_record? - jt_conditions << - join_table[through_reflection.active_record.inheritance_column]. - eq(through_reflection.klass.sti_name) - end - when :belongs_to - first_key = primary_key - - if reflection.options[:source_type] - second_key = source_reflection.association_foreign_key - - jt_conditions << - join_table[reflection.source_reflection.options[:foreign_type]]. - eq(reflection.options[:source_type]) - else - second_key = source_reflection.primary_key_name - end - end - - jt_conditions << - parent_table[parent.primary_key]. - eq(join_table[jt_foreign_key]) - - if through_reflection.options[:conditions] - jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name) - end - - relation = relation.join(join_table, join_type).on(*jt_conditions) - - join_target_table( - relation, - target_table[first_key].eq(join_table[second_key]) - ) - end - - def join_has_many_polymorphic_to(relation) - join_target_table( - relation, - target_table["#{reflection.options[:as]}_id"]. - eq(parent_table[parent.primary_key]), - target_table["#{reflection.options[:as]}_type"]. - eq(parent.active_record.base_class.name) - ) - end - - def join_belongs_to_to(relation) - foreign_key = options[:foreign_key] || reflection.primary_key_name - primary_key = options[:primary_key] || reflection.klass.primary_key - - join_target_table( - relation, - target_table[primary_key].eq(parent_table[foreign_key]) - ) - end - end - end end end end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index cb2d9e0a79..ba9373ba6a 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -19,11 +19,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class AssociationCollection < AssociationProxy #:nodoc: - def initialize(owner, reflection) - super - construct_sql - end - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped def select(select = nil) @@ -36,7 +31,7 @@ module ActiveRecord end def scoped - with_scope(construct_scope) { @reflection.klass.scoped } + with_scope(@scope) { @reflection.klass.scoped } end def find(*args) @@ -58,9 +53,7 @@ module ActiveRecord merge_options_from_reflection!(options) construct_find_options!(options) - find_scope = construct_scope[:find].slice(:conditions, :order) - - with_scope(:find => find_scope) do + with_scope(:find => @scope[:find].slice(:conditions, :order)) do relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods)) case args.first @@ -82,6 +75,7 @@ module ActiveRecord find(:first, *args) else load_target unless loaded? + args = args[1..-1] if args.first.kind_of?(Hash) && args.first.empty? @target.first(*args) end end @@ -127,13 +121,13 @@ module ActiveRecord # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. def <<(*records) result = true - load_target if @owner.new_record? + load_target unless @owner.persisted? transaction do flatten_deeper(records).each do |record| raise_on_type_mismatch(record) add_record_to_target_with_callbacks(record) do |r| - result &&= insert_record(record) unless @owner.new_record? + result &&= insert_record(record) if @owner.persisted? end end end @@ -178,17 +172,18 @@ module ActiveRecord end end - # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will - # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the - # descendant's +construct_sql+ method will have set :counter_sql automatically. - # Otherwise, construct options and pass them with scope to the target class's +count+. + # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the + # association, it will be used for the query. Otherwise, construct options and pass them with + # scope to the target class's +count+. def count(column_name = nil, options = {}) column_name, options = nil, column_name if column_name.is_a?(Hash) - if @reflection.options[:counter_sql] && !options.blank? - raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" - elsif @reflection.options[:counter_sql] - @reflection.klass.count_by_sql(@counter_sql) + if @reflection.options[:counter_sql] || @reflection.options[:finder_sql] + unless options.blank? + raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" + end + + @reflection.klass.count_by_sql(custom_counter_sql) else if @reflection.options[:uniq] @@ -197,7 +192,7 @@ module ActiveRecord options.merge!(:distinct => true) end - value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) } + value = @reflection.klass.send(:with_scope, @scope) { @reflection.klass.count(column_name, options) } limit = @reflection.options[:limit] offset = @reflection.options[:offset] @@ -240,12 +235,12 @@ module ActiveRecord # Removes all records from this association. Returns +self+ so method calls may be chained. def clear - return self if length.zero? # forces load_target if it hasn't happened already - - if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy - destroy_all - else - delete_all + unless length.zero? # forces load_target if it hasn't happened already + if @reflection.options[:dependent] == :destroy + destroy_all + else + delete_all + end end self @@ -291,12 +286,12 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if @owner.new_record? || (loaded? && !@reflection.options[:uniq]) + if !@owner.persisted? || (loaded? && !@reflection.options[:uniq]) @target.size elsif !loaded? && @reflection.options[:group] load_target.size elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array) - unsaved_records = @target.select { |r| r.new_record? } + unsaved_records = @target.reject { |r| r.persisted? } unsaved_records.size + count_records else count_records @@ -337,13 +332,10 @@ module ActiveRecord end def uniq(collection = self) - seen = Set.new - collection.map do |record| - unless seen.include?(record.id) - seen << record.id - record - end - end.compact + seen = {} + collection.find_all do |record| + seen[record.id] = true unless seen.key?(record.id) + end end # Replace this collection with +other_array+ @@ -363,10 +355,9 @@ module ActiveRecord def include?(record) return false unless record.is_a?(@reflection.klass) - return include_in_memory?(record) if record.new_record? + return include_in_memory?(record) unless record.persisted? load_target if @reflection.options[:finder_sql] && !loaded? - return @target.include?(record) if loaded? - exists?(record) + loaded? ? @target.include?(record) : exists?(record) end def proxy_respond_to?(method, include_private = false) @@ -377,29 +368,19 @@ module ActiveRecord def construct_find_options!(options) end - def construct_counter_sql - if @reflection.options[:counter_sql] - @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) - elsif @reflection.options[:finder_sql] - # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ - @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } - @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) - else - @counter_sql = @finder_sql - end - end - def load_target - if !@owner.new_record? || foreign_key_present + if @owner.persisted? || foreign_key_present begin - if !loaded? + unless loaded? if @target.is_a?(Array) && @target.any? @target = find_target.map do |f| i = @target.index(f) if i @target.delete_at(i).tap do |t| keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) - t.attributes = f.attributes.except(*keys) + f.attributes.except(*keys).each do |k,v| + t.send("#{k}=", v) + end end else f @@ -426,17 +407,13 @@ module ActiveRecord end if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) - if block_given? - super { |*block_args| yield(*block_args) } - else - super - end + super elsif @reflection.klass.scopes[method] @_named_scopes_cache ||= {} @_named_scopes_cache[method] ||= {} - @_named_scopes_cache[method][args] ||= with_scope(construct_scope) { @reflection.klass.send(method, *args) } + @_named_scopes_cache[method][args] ||= with_scope(@scope) { @reflection.klass.send(method, *args) } else - with_scope(construct_scope) do + with_scope(@scope) do if block_given? @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) } else @@ -446,9 +423,19 @@ module ActiveRecord end end - # overloaded in derived Association classes to provide useful scoping depending on association type. - def construct_scope - {} + def custom_counter_sql + if @reflection.options[:counter_sql] + counter_sql = @reflection.options[:counter_sql] + else + # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ + counter_sql = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + end + + interpolate_sql(counter_sql) + end + + def custom_finder_sql + interpolate_sql(@reflection.options[:finder_sql]) end def reset_target! @@ -462,7 +449,7 @@ module ActiveRecord def find_target records = if @reflection.options[:finder_sql] - @reflection.klass.find_by_sql(@finder_sql) + @reflection.klass.find_by_sql(custom_finder_sql) else find(:all) end @@ -494,7 +481,7 @@ module ActiveRecord ensure_owner_is_not_new scoped_where = scoped.where_values_hash - create_scope = scoped_where ? construct_scope[:create].merge(scoped_where) : construct_scope[:create] + create_scope = scoped_where ? @scope[:create].merge(scoped_where) : @scope[:create] record = @reflection.klass.send(:with_scope, :create => create_scope) do @reflection.build_association(attrs) end @@ -521,7 +508,7 @@ module ActiveRecord transaction do records.each { |record| callback(:before_remove, record) } - old_records = records.reject { |r| r.new_record? } + old_records = records.select { |r| r.persisted? } yield(records, old_records) records.each { |record| callback(:after_remove, record) } end @@ -542,18 +529,18 @@ 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) || [] + @owner.class.send(full_callback_name.to_sym) || [] end def ensure_owner_is_not_new - if @owner.new_record? + unless @owner.persisted? raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" end end def fetch_first_or_last_using_find?(args) - args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || - @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer)) + (args.first.kind_of?(Hash) && !args.first.empty?) || !(loaded? || !@owner.persisted? || @reflection.options[:finder_sql] || + !@target.all? { |record| record.persisted? } || args.first.kind_of?(Integer)) end def include_in_memory?(record) diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index f333f4d603..7cd04a1ad5 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -53,7 +53,7 @@ module ActiveRecord alias_method :proxy_respond_to?, :respond_to? alias_method :proxy_extend, :extend delegate :to_param, :to => :proxy_target - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|proxy_/ } + instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to_missing|proxy_/ } def initialize(owner, reflection) @owner, @reflection = owner, reflection @@ -61,6 +61,7 @@ module ActiveRecord reflection.check_validity! Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) } reset + construct_scope end # Returns the owner of the proxy. @@ -174,10 +175,10 @@ module ActiveRecord # If the association is polymorphic the type of the owner is also set. def set_belongs_to_association_for(record) if @reflection.options[:as] - record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record? + record["#{@reflection.options[:as]}_id"] = @owner.id if @owner.persisted? record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s else - unless @owner.new_record? + if @owner.persisted? primary_key = @reflection.options[:primary_key] || :id record[@reflection.primary_key_name] = @owner.send(primary_key) end @@ -203,6 +204,24 @@ module ActiveRecord @reflection.klass.send :with_scope, *args, &block end + # Construct the scope used for find/create queries on the target + def construct_scope + @scope = { + :find => construct_find_scope, + :create => construct_create_scope + } + end + + # Implemented by subclasses + def construct_find_scope + raise NotImplementedError + end + + # Implemented by (some) subclasses + def construct_create_scope + {} + end + private # Forwards any missing method call to the \target. def method_missing(method, *args) @@ -233,7 +252,7 @@ module ActiveRecord def load_target return nil unless defined?(@loaded) - if !loaded? and (!@owner.new_record? || foreign_key_present) + if !loaded? and (@owner.persisted? || foreign_key_present) @target = find_target end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 2eb56e5cd3..b438620c8f 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -14,7 +14,7 @@ module ActiveRecord counter_cache_name = @reflection.counter_cache_column if record.nil? - if counter_cache_name && !@owner.new_record? + if counter_cache_name && @owner.persisted? @reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name] end @@ -22,13 +22,13 @@ module ActiveRecord else raise_on_type_mismatch(record) - if counter_cache_name && !@owner.new_record? && record.id != @owner[@reflection.primary_key_name] + if counter_cache_name && @owner.persisted? && record.id != @owner[@reflection.primary_key_name] @reflection.klass.increment_counter(counter_cache_name, record.id) @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name] end @target = (AssociationProxy === record ? record.target : record) - @owner[@reflection.primary_key_name] = record_id(record) unless record.new_record? + @owner[@reflection.primary_key_name] = record_id(record) if record.persisted? @updated = true end @@ -50,20 +50,22 @@ module ActiveRecord "find" end - options = @reflection.options.dup - (options.keys - [:select, :include, :readonly]).each do |key| - options.delete key - end - options[:conditions] = conditions + options = @reflection.options.dup.slice(:select, :include, :readonly) - the_target = @reflection.klass.send(find_method, - @owner[@reflection.primary_key_name], - options - ) if @owner[@reflection.primary_key_name] + the_target = with_scope(:find => @scope[:find]) do + @reflection.klass.send(find_method, + @owner[@reflection.primary_key_name], + options + ) if @owner[@reflection.primary_key_name] + end set_inverse_instance(the_target, @owner) the_target end + def construct_find_scope + { :conditions => conditions } + end + def foreign_key_present !@owner[@reflection.primary_key_name].nil? end 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 e429806b0c..a0df860623 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -44,20 +44,20 @@ module ActiveRecord end end + def construct_find_scope + { :conditions => conditions } + end + def find_target return nil if association_class.nil? - target = - if @reflection.options[:conditions] - association_class.find( - @owner[@reflection.primary_key_name], - :select => @reflection.options[:select], - :conditions => conditions, - :include => @reflection.options[:include] - ) - else - association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include]) - end + target = association_class.send(:with_scope, :find => @scope[:find]) do + association_class.find( + @owner[@reflection.primary_key_name], + :select => @reflection.options[:select], + :include => @reflection.options[:include] + ) + end set_inverse_instance(target, @owner) target end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb new file mode 100644 index 0000000000..6ab7bd0b06 --- /dev/null +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb @@ -0,0 +1,225 @@ +require 'active_record/associations/class_methods/join_dependency/join_part' +require 'active_record/associations/class_methods/join_dependency/join_base' +require 'active_record/associations/class_methods/join_dependency/join_association' + +module ActiveRecord + module Associations + module ClassMethods + class JoinDependency # :nodoc: + attr_reader :join_parts, :reflections, :table_aliases + + def initialize(base, associations, joins) + @join_parts = [JoinBase.new(base, joins)] + @associations = {} + @reflections = [] + @table_aliases = Hash.new(0) + @table_aliases[base.table_name] = 1 + build(associations) + end + + def graft(*associations) + associations.each do |association| + join_associations.detect {|a| association == a} || + build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type) + end + self + end + + def join_associations + join_parts.last(join_parts.length - 1) + end + + def join_base + join_parts.first + end + + def columns + join_parts.collect { |join_part| + table = join_part.aliased_table + join_part.column_names_with_alias.collect{ |column_name, aliased_name| + table[column_name].as Arel.sql(aliased_name) + } + }.flatten + end + + def count_aliases_from_table_joins(name) + # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase + quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase + join_sql = join_base.table_joins.to_s.downcase + join_sql.blank? ? 0 : + # Table names + join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size + + # Table aliases + join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size + end + + def instantiate(rows) + primary_key = join_base.aliased_primary_key + parents = {} + + records = rows.map { |model| + primary_id = model[primary_key] + parent = parents[primary_id] ||= join_base.instantiate(model) + construct(parent, @associations, join_associations.dup, model) + parent + }.uniq + + remove_duplicate_results!(join_base.active_record, records, @associations) + records + end + + def remove_duplicate_results!(base, records, associations) + case associations + when Symbol, String + reflection = base.reflections[associations] + remove_uniq_by_reflection(reflection, records) + when Array + associations.each do |association| + remove_duplicate_results!(base, records, association) + end + when Hash + associations.keys.each do |name| + reflection = base.reflections[name] + remove_uniq_by_reflection(reflection, records) + + parent_records = [] + records.each do |record| + if descendant = record.send(reflection.name) + if reflection.collection? + parent_records.concat descendant.target.uniq + else + parent_records << descendant + end + end + end + + remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? + end + end + end + + protected + + def cache_joined_association(association) + associations = [] + parent = association.parent + while parent != join_base + associations.unshift(parent.reflection.name) + parent = parent.parent + end + ref = @associations + associations.each do |key| + ref = ref[key] + end + ref[association.reflection.name] ||= {} + end + + def build(associations, parent = nil, join_type = Arel::InnerJoin) + parent ||= join_parts.last + case associations + when Symbol, String + reflection = parent.reflections[associations.to_s.intern] or + raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" + unless join_association = find_join_association(reflection, parent) + @reflections << reflection + join_association = build_join_association(reflection, parent) + join_association.join_type = join_type + @join_parts << join_association + cache_joined_association(join_association) + end + join_association + when Array + associations.each do |association| + build(association, parent, join_type) + end + when Hash + associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| + join_association = build(name, parent, join_type) + build(associations[name], join_association, join_type) + end + else + raise ConfigurationError, associations.inspect + end + end + + def find_join_association(name_or_reflection, parent) + if String === name_or_reflection + name_or_reflection = name_or_reflection.to_sym + end + + join_associations.detect { |j| + j.reflection == name_or_reflection && j.parent == parent + } + end + + def remove_uniq_by_reflection(reflection, records) + if reflection && reflection.collection? + records.each { |record| record.send(reflection.name).target.uniq! } + end + end + + def build_join_association(reflection, parent) + JoinAssociation.new(reflection, self, parent) + end + + def construct(parent, associations, join_parts, row) + case associations + when Symbol, String + name = associations.to_s + + join_part = join_parts.detect { |j| + j.reflection.name.to_s == name && + j.parent_table_name == parent.class.table_name } + + raise(ConfigurationError, "No such association") unless join_part + + join_parts.delete(join_part) + construct_association(parent, join_part, row) + when Array + associations.each do |association| + construct(parent, association, join_parts, row) + end + when Hash + associations.sort_by { |k,_| k.to_s }.each do |name, assoc| + association = construct(parent, name, join_parts, row) + construct(association, assoc, join_parts, row) if association + end + else + raise ConfigurationError, associations.inspect + end + end + + def construct_association(record, join_part, row) + return if record.id.to_s != join_part.parent.record_id(row).to_s + + macro = join_part.reflection.macro + if macro == :has_one + return if record.instance_variable_defined?("@#{join_part.reflection.name}") + association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? + set_target_and_inverse(join_part, association, record) + else + return if row[join_part.aliased_primary_key].nil? + association = join_part.instantiate(row) + case macro + when :has_many, :has_and_belongs_to_many + collection = record.send(join_part.reflection.name) + collection.loaded + collection.target.push(association) + collection.__send__(:set_inverse_instance, association, record) + when :belongs_to + set_target_and_inverse(join_part, association, record) + else + raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" + end + end + association + end + + def set_target_and_inverse(join_part, association, record) + association_proxy = record.send("set_#{join_part.reflection.name}_target", association) + association_proxy.__send__(:set_inverse_instance, association, record) + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb new file mode 100644 index 0000000000..5e5c01c77a --- /dev/null +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb @@ -0,0 +1,278 @@ +module ActiveRecord + module Associations + module ClassMethods + class JoinDependency # :nodoc: + class JoinAssociation < JoinPart # :nodoc: + # The reflection of the association represented + attr_reader :reflection + + # The JoinDependency object which this JoinAssociation exists within. This is mainly + # relevant for generating aliases which do not conflict with other joins which are + # part of the query. + attr_reader :join_dependency + + # A JoinBase instance representing the active record we are joining onto. + # (So in Author.has_many :posts, the Author would be that base record.) + attr_reader :parent + + # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin + attr_accessor :join_type + + # These implement abstract methods from the superclass + attr_reader :aliased_prefix, :aliased_table_name + + delegate :options, :through_reflection, :source_reflection, :to => :reflection + delegate :table, :table_name, :to => :parent, :prefix => true + + def initialize(reflection, join_dependency, parent = nil) + reflection.check_validity! + + if reflection.options[:polymorphic] + raise EagerLoadPolymorphicError.new(reflection) + end + + super(reflection.klass) + + @reflection = reflection + @join_dependency = join_dependency + @parent = parent + @join_type = Arel::InnerJoin + + # This must be done eagerly upon initialisation because the alias which is produced + # depends on the state of the join dependency, but we want it to work the same way + # every time. + allocate_aliases + end + + def ==(other) + other.class == self.class && + other.reflection == reflection && + other.parent == parent + end + + def find_parent_in(other_join_dependency) + other_join_dependency.join_parts.detect do |join_part| + self.parent == join_part + end + end + + def join_to(relation) + send("join_#{reflection.macro}_to", relation) + end + + def join_relation(joining_relation) + self.join_type = Arel::OuterJoin + joining_relation.joins(self) + end + + def table + @table ||= Arel::Table.new( + table_name, :as => aliased_table_name, + :engine => arel_engine, :columns => active_record.columns + ) + end + + # More semantic name given we are talking about associations + alias_method :target_table, :table + + protected + + def aliased_table_name_for(name, suffix = nil) + if @join_dependency.table_aliases[name].zero? + @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name) + end + + if !@join_dependency.table_aliases[name].zero? # We need an alias + name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" + @join_dependency.table_aliases[name] += 1 + if @join_dependency.table_aliases[name] == 1 # First time we've seen this name + # Also need to count the aliases from the table_aliases to avoid incorrect count + @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name) + end + table_index = @join_dependency.table_aliases[name] + name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1 + else + @join_dependency.table_aliases[name] += 1 + end + + name + end + + def pluralize(table_name) + ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + end + + def interpolate_sql(sql) + instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) + end + + private + + def allocate_aliases + @aliased_prefix = "t#{ join_dependency.join_parts.size }" + @aliased_table_name = aliased_table_name_for(table_name) + + if reflection.macro == :has_and_belongs_to_many + @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") + elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] + @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") + end + end + + def process_conditions(conditions, table_name) + Arel.sql(interpolate_sql(sanitize_sql(conditions, table_name))) + end + + def join_target_table(relation, *conditions) + relation = relation.join(target_table, join_type) + + # If the target table is an STI model then we must be sure to only include records of + # its type and its sub-types. + unless active_record.descends_from_active_record? + sti_column = target_table[active_record.inheritance_column] + + sti_condition = sti_column.eq(active_record.sti_name) + active_record.descendants.each do |subclass| + sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) + end + + conditions << sti_condition + end + + # If the reflection has conditions, add them + if options[:conditions] + conditions << process_conditions(options[:conditions], aliased_table_name) + end + + relation = relation.on(*conditions) + end + + def join_has_and_belongs_to_many_to(relation) + join_table = Arel::Table.new( + options[:join_table], :engine => arel_engine, + :as => @aliased_join_table_name + ) + + fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key + klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key + + relation = relation.join(join_table, join_type) + relation = relation.on( + join_table[fk]. + eq(parent_table[reflection.active_record.primary_key]) + ) + + join_target_table( + relation, + target_table[reflection.klass.primary_key]. + eq(join_table[klass_fk]) + ) + end + + def join_has_many_to(relation) + if reflection.options[:through] + join_has_many_through_to(relation) + elsif reflection.options[:as] + join_has_many_polymorphic_to(relation) + else + foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key + primary_key = options[:primary_key] || parent.primary_key + + join_target_table( + relation, + target_table[foreign_key]. + eq(parent_table[primary_key]) + ) + end + end + alias :join_has_one_to :join_has_many_to + + def join_has_many_through_to(relation) + join_table = Arel::Table.new( + through_reflection.klass.table_name, :engine => arel_engine, + :as => @aliased_join_table_name + ) + + jt_conditions = [] + jt_foreign_key = first_key = second_key = nil + + if through_reflection.options[:as] # has_many :through against a polymorphic join + as_key = through_reflection.options[:as].to_s + jt_foreign_key = as_key + '_id' + + jt_conditions << + join_table[as_key + '_type']. + eq(parent.active_record.base_class.name) + else + jt_foreign_key = through_reflection.primary_key_name + end + + case source_reflection.macro + when :has_many + second_key = options[:foreign_key] || primary_key + + if source_reflection.options[:as] + first_key = "#{source_reflection.options[:as]}_id" + else + first_key = through_reflection.klass.base_class.to_s.foreign_key + end + + unless through_reflection.klass.descends_from_active_record? + jt_conditions << + join_table[through_reflection.active_record.inheritance_column]. + eq(through_reflection.klass.sti_name) + end + when :belongs_to + first_key = primary_key + + if reflection.options[:source_type] + second_key = source_reflection.association_foreign_key + + jt_conditions << + join_table[reflection.source_reflection.options[:foreign_type]]. + eq(reflection.options[:source_type]) + else + second_key = source_reflection.primary_key_name + end + end + + jt_conditions << + parent_table[parent.primary_key]. + eq(join_table[jt_foreign_key]) + + if through_reflection.options[:conditions] + jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name) + end + + relation = relation.join(join_table, join_type).on(*jt_conditions) + + join_target_table( + relation, + target_table[first_key].eq(join_table[second_key]) + ) + end + + def join_has_many_polymorphic_to(relation) + join_target_table( + relation, + target_table["#{reflection.options[:as]}_id"]. + eq(parent_table[parent.primary_key]), + target_table["#{reflection.options[:as]}_type"]. + eq(parent.active_record.base_class.name) + ) + end + + def join_belongs_to_to(relation) + foreign_key = options[:foreign_key] || reflection.primary_key_name + primary_key = options[:primary_key] || reflection.klass.primary_key + + join_target_table( + relation, + target_table[primary_key].eq(parent_table[foreign_key]) + ) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb new file mode 100644 index 0000000000..ed05003f66 --- /dev/null +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module Associations + module ClassMethods + class JoinDependency # :nodoc: + class JoinBase < JoinPart # :nodoc: + # Extra joins provided when the JoinDependency was created + attr_reader :table_joins + + def initialize(active_record, joins = nil) + super(active_record) + @table_joins = joins + end + + def ==(other) + other.class == self.class && + other.active_record == active_record + end + + def aliased_prefix + "t0" + end + + def table + Arel::Table.new(table_name, :engine => arel_engine, :columns => active_record.columns) + end + + def aliased_table_name + active_record.table_name + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb new file mode 100644 index 0000000000..0b093b65e9 --- /dev/null +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb @@ -0,0 +1,80 @@ +module ActiveRecord + module Associations + module ClassMethods + class JoinDependency # :nodoc: + # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited + # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which + # everything else is being joined onto. A JoinAssociation represents an association which + # is joining to the base. A JoinAssociation may result in more than one actual join + # operations (for example a has_and_belongs_to_many JoinAssociation would result in + # two; one for the join table and one for the target table). + class JoinPart # :nodoc: + # The Active Record class which this join part is associated 'about'; for a JoinBase + # this is the actual base model, for a JoinAssociation this is the target model of the + # association. + attr_reader :active_record + + delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record + + def initialize(active_record) + @active_record = active_record + @cached_record = {} + @column_names_with_alias = nil + end + + def aliased_table + Arel::Nodes::TableAlias.new aliased_table_name, table + end + + def ==(other) + raise NotImplementedError + end + + # An Arel::Table for the active_record + def table + raise NotImplementedError + end + + # The prefix to be used when aliasing columns in the active_record's table + def aliased_prefix + raise NotImplementedError + end + + # The alias for the active_record's table + def aliased_table_name + raise NotImplementedError + end + + # The alias for the primary key of the active_record's table + def aliased_primary_key + "#{aliased_prefix}_r0" + end + + # An array of [column_name, alias] pairs for the table + def column_names_with_alias + unless @column_names_with_alias + @column_names_with_alias = [] + + ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| + @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] + end + end + @column_names_with_alias + end + + def extract_record(row) + Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}] + end + + def record_id(row) + row[aliased_primary_key] + end + + def instantiate(row) + @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row)) + end + end + end + end + end +end 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 eb65234dfb..2c72fd0004 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 @@ -24,7 +24,7 @@ module ActiveRecord protected def construct_find_options!(options) - options[:joins] = Arel::SqlLiteral.new @join_sql + options[:joins] = Arel::SqlLiteral.new(@scope[:find][:joins]) options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select]) options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*')) end @@ -34,7 +34,7 @@ module ActiveRecord end def insert_record(record, force = true, validate = true) - if record.new_record? + unless record.persisted? if force record.save! else @@ -67,7 +67,7 @@ module ActiveRecord relation.insert(attributes) end - return true + true end def delete_records(records) @@ -81,26 +81,25 @@ module ActiveRecord end end - def construct_sql - if @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - else - @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " - @finder_sql << " AND (#{conditions})" if conditions - end - - @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" + def construct_joins + "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" + end - construct_counter_sql + def construct_conditions + sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " + sql << " AND (#{conditions})" if conditions + sql end - def construct_scope - { :find => { :conditions => @finder_sql, - :joins => @join_sql, - :readonly => false, - :order => @reflection.options[:order], - :include => @reflection.options[:include], - :limit => @reflection.options[:limit] } } + def construct_find_scope + { + :conditions => construct_conditions, + :joins => construct_joins, + :readonly => false, + :order => @reflection.options[:order], + :include => @reflection.options[:include], + :limit => @reflection.options[:limit] + } end # Join tables with additional columns on top of the two foreign keys must be considered diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 978fc74560..685d818ab3 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -6,14 +6,10 @@ module ActiveRecord # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. class HasManyAssociation < AssociationCollection #:nodoc: - def initialize(owner, reflection) - @finder_sql = nil - super - end protected def owner_quoted_id if @reflection.options[:primary_key] - quote_value(@owner.send(@reflection.options[:primary_key])) + @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) else @owner.quoted_id end @@ -35,10 +31,10 @@ module ActiveRecord def count_records count = if has_cached_counter? @owner.send(:read_attribute, cached_counter_attribute_name) - elsif @reflection.options[:counter_sql] - @reflection.klass.count_by_sql(@counter_sql) + elsif @reflection.options[:counter_sql] || @reflection.options[:finder_sql] + @reflection.klass.count_by_sql(custom_counter_sql) else - @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include]) + @reflection.klass.count(@scope[:find].slice(:conditions, :joins, :include)) end # If there's nothing in the database and @target has no new records @@ -46,11 +42,7 @@ module ActiveRecord # documented side-effect of the method that may avoid an extra SELECT. @target ||= [] and loaded if count == 0 - if @reflection.options[:limit] - count = [ @reflection.options[:limit], count ].min - end - - return count + [@reflection.options[:limit], count].compact.min end def has_cached_counter? @@ -87,41 +79,36 @@ module ActiveRecord false end - def construct_sql - case - when @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - - when @reflection.options[:as] - @finder_sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - @finder_sql << " AND (#{conditions})" if conditions - - else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - @finder_sql << " AND (#{conditions})" if conditions + def construct_conditions + if @reflection.options[:as] + sql = + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" + else + sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" end + sql << " AND (#{conditions})" if conditions + sql + end - construct_counter_sql + def construct_find_scope + { + :conditions => construct_conditions, + :readonly => false, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :include => @reflection.options[:include] + } end - def construct_scope + def construct_create_scope 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]}, - :create => create_scoping - } + create_scoping end def we_can_set_the_inverse_on_this?(record) - inverse = @reflection.inverse_of - return !inverse.nil? + @reflection.inverse_of end end 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 97883d8393..79c229d9c4 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -59,7 +59,7 @@ module ActiveRecord end def insert_record(record, force = true, validate = true) - if record.new_record? + unless record.persisted? if force record.save! else @@ -68,8 +68,7 @@ module ActiveRecord end through_association = @owner.send(@reflection.through_reflection.name) - through_record = through_association.create!(construct_join_attributes(record)) - through_association.proxy_target << through_record + through_association.create!(construct_join_attributes(record)) end # TODO - add dependent option support @@ -82,21 +81,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - with_scope(construct_scope) { @reflection.klass.find(:all) } - end - - def construct_sql - case - when @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - @finder_sql << " AND (#{conditions})" if conditions - else - @finder_sql = construct_conditions - end - - construct_counter_sql + with_scope(@scope) { @reflection.klass.find(:all) } end def has_cached_counter? diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index a6e6bfa356..e6e037441f 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -2,11 +2,6 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < AssociationProxy #:nodoc: - def initialize(owner, reflection) - super - construct_sql - end - def create(attrs = {}, replace_existing = true) new_record(replace_existing) do |reflection| attrs = merge_with_conditions(attrs) @@ -35,18 +30,18 @@ module ActiveRecord if dependent? && !dont_save case @reflection.options[:dependent] when :delete - @target.delete unless @target.new_record? + @target.delete if @target.persisted? @owner.clear_association_cache when :destroy - @target.destroy unless @target.new_record? + @target.destroy if @target.persisted? @owner.clear_association_cache when :nullify @target[@reflection.primary_key_name] = nil - @target.save unless @owner.new_record? || @target.new_record? + @target.save if @owner.persisted? && @target.persisted? end else @target[@reflection.primary_key_name] = nil - @target.save unless @owner.new_record? || @target.new_record? + @target.save if @owner.persisted? && @target.persisted? end end @@ -61,7 +56,7 @@ module ActiveRecord set_inverse_instance(obj, @owner) @loaded = true - unless @owner.new_record? or obj.nil? or dont_save + unless !@owner.persisted? or obj.nil? or dont_save return (obj.save ? self : false) else return (obj.nil? ? nil : self) @@ -79,33 +74,31 @@ module ActiveRecord private def find_target - options = @reflection.options.dup - (options.keys - [:select, :order, :include, :readonly]).each do |key| - options.delete key - end - options[:conditions] = @finder_sql + options = @reflection.options.dup.slice(:select, :order, :include, :readonly) - the_target = @reflection.klass.find(:first, options) + the_target = with_scope(:find => @scope[:find]) do + @reflection.klass.find(:first, options) + end set_inverse_instance(the_target, @owner) the_target end - def construct_sql - case - when @reflection.options[:as] - @finder_sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" + def construct_find_scope + if @reflection.options[:as] + sql = + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" + else + sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" end - @finder_sql << " AND (#{conditions})" if conditions + sql << " AND (#{conditions})" if conditions + { :conditions => sql } end - def construct_scope + def construct_create_scope create_scoping = {} set_belongs_to_association_for(create_scoping) - { :create => create_scoping } + create_scoping end def new_record(replace_existing) @@ -113,14 +106,14 @@ module ActiveRecord # instance. Otherwise, if the target has not previously been loaded # elsewhere, the instance we create will get orphaned. load_target if replace_existing - record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do + record = @reflection.klass.send(:with_scope, :create => @scope[:create]) do yield @reflection end if replace_existing replace(record, true) else - record[@reflection.primary_key_name] = @owner.id unless @owner.new_record? + record[@reflection.primary_key_name] = @owner.id if @owner.persisted? self.target = record set_inverse_instance(record, @owner) end 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 fba0a2bfcc..6e98f7dffb 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -21,7 +21,7 @@ module ActiveRecord if current_object new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy elsif new_value - if @owner.new_record? + unless @owner.persisted? self.target = new_value through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name) through_association.build(construct_join_attributes(new_value)) @@ -33,7 +33,7 @@ module ActiveRecord private def find_target - with_scope(construct_scope) { @reflection.klass.find(:first) } + with_scope(@scope) { @reflection.klass.find(:first) } end end end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index cabb33c4a8..acddfda924 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -5,16 +5,20 @@ module ActiveRecord protected - def construct_scope - { :create => construct_owner_attributes(@reflection), - :find => { :conditions => construct_conditions, - :joins => construct_joins, - :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], - :select => construct_select, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :readonly => @reflection.options[:readonly], - } } + def construct_find_scope + { + :conditions => construct_conditions, + :joins => construct_joins, + :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], + :select => construct_select, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :readonly => @reflection.options[:readonly] + } + end + + def construct_create_scope + construct_owner_attributes(@reflection) end # Build SQL conditions from attributes, qualified by table name. @@ -47,7 +51,7 @@ module ActiveRecord def construct_select(custom_select = nil) distinct = "DISTINCT " if @reflection.options[:uniq] - selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*" + custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*" end def construct_joins(custom_joins = nil) diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 439880c1fa..c19a33faa8 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/object/blank' module ActiveRecord @@ -88,7 +89,7 @@ module ActiveRecord end def clone_with_time_zone_conversion_attribute?(attr, old) - old.class.name == "Time" && time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(attr.to_sym) + old.class.name == "Time" && time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(attr.to_sym) end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 82d94b848a..75ae06f5e9 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -3,10 +3,11 @@ module ActiveRecord module PrimaryKey extend ActiveSupport::Concern - # Returns this record's primary key value wrapped in an Array - # or nil if the record is a new_record? + # Returns this record's primary key value wrapped in an Array or nil if + # the record is not persisted? or has just been destroyed. def to_key - new_record? ? nil : [ id ] + key = send(self.class.primary_key) + [key] if key end module ClassMethods diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index d640b26b74..dc2785b6bf 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/class/attribute' + module ActiveRecord module AttributeMethods module TimeZoneConversion @@ -7,7 +9,7 @@ module ActiveRecord cattr_accessor :time_zone_aware_attributes, :instance_writer => false self.time_zone_aware_attributes = false - class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false + class_attribute :skip_time_zone_conversion_for_attributes, :instance_writer => false self.skip_time_zone_conversion_for_attributes = [] end @@ -54,7 +56,7 @@ module ActiveRecord private def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) + time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 4a2c078e91..73ac8e82c6 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -89,7 +89,7 @@ module ActiveRecord # post = Post.create(:title => 'ruby rocks') # post.comments.create(:body => 'hello world') # post.comments[0].body = 'hi everyone' - # post.save # => saves both post and comment, with 'hi everyone' as title + # post.save # => saves both post and comment, with 'hi everyone' as body # # Destroying one of the associated models as part of the parent's save action # is as simple as marking it for destruction: @@ -217,7 +217,7 @@ module ActiveRecord # Returns whether or not this record has been changed in any way (including whether # any of its nested autosave associations are likewise changed) def changed_for_autosave? - new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave? + !persisted? || changed? || marked_for_destruction? || nested_records_changed_for_autosave? end private @@ -231,7 +231,7 @@ module ActiveRecord elsif autosave association.target.find_all { |record| record.changed_for_autosave? } else - association.target.find_all { |record| record.new_record? } + association.target.find_all { |record| !record.persisted? } end end @@ -257,7 +257,7 @@ module ActiveRecord # +reflection+. def validate_collection_association(reflection) if association = association_instance_get(reflection.name) - if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) + if records = associated_records_to_validate_or_save(association, !persisted?, reflection.options[:autosave]) records.each { |record| association_valid?(reflection, record) } end end @@ -286,7 +286,7 @@ module ActiveRecord # Is used as a before_save callback to check while saving a collection # association whether or not the parent was a new record before saving. def before_save_collection_association - @new_record_before_save = new_record? + @new_record_before_save = !persisted? true end @@ -308,7 +308,7 @@ module ActiveRecord if autosave && record.marked_for_destruction? association.destroy(record) - elsif autosave != false && (@new_record_before_save || record.new_record?) + elsif autosave != false && (@new_record_before_save || !record.persisted?) if autosave saved = association.send(:insert_record, record, false, false) else @@ -322,8 +322,8 @@ module ActiveRecord end end - # reconstruct the SQL queries now that we know the owner's id - association.send(:construct_sql) if association.respond_to?(:construct_sql) + # reconstruct the scope now that we know the owner's id + association.send(:construct_scope) if association.respond_to?(:construct_scope) end end @@ -343,7 +343,7 @@ module ActiveRecord association.destroy else key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave) + if autosave != false && (!persisted? || !association.persisted? || association[reflection.primary_key_name] != key || autosave) association[reflection.primary_key_name] = key saved = association.save(:validate => !autosave) raise ActiveRecord::Rollback if !saved && autosave @@ -363,7 +363,7 @@ module ActiveRecord if autosave && association.marked_for_destruction? association.destroy elsif autosave != false - saved = association.save(:validate => !autosave) if association.new_record? || autosave + saved = association.save(:validate => !autosave) if !association.persisted? || autosave if association.updated? association_id = association.send(reflection.options[:primary_key] || :id) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 053b796991..9b09b14c87 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -7,7 +7,7 @@ require 'active_support/time' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/delegating_attributes' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/indifferent_access' @@ -204,7 +204,7 @@ module ActiveRecord #:nodoc: # # # No 'Winter' tag exists # winter = Tag.find_or_initialize_by_name("Winter") - # winter.new_record? # true + # winter.persisted? # false # # To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of # a list of parameters. @@ -412,7 +412,7 @@ module ActiveRecord #:nodoc: self.store_full_sti_class = true # Stores the default scope for the class - class_inheritable_accessor :default_scoping, :instance_writer => false + class_attribute :default_scoping, :instance_writer => false self.default_scoping = [] # Returns a hash of all the attributes that have been specified for serialization as @@ -420,6 +420,9 @@ module ActiveRecord #:nodoc: class_attribute :serialized_attributes self.serialized_attributes = {} + class_attribute :_attr_readonly, :instance_writer => false + self._attr_readonly = [] + class << self # Class methods 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 @@ -448,8 +451,8 @@ module ActiveRecord #:nodoc: # # You can use the same string replacement techniques as you can with ActiveRecord#find # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # > [#<Post:0x36bff9c @attributes={"first_name"=>"The Cheap Man Buys Twice"}>, ...] - def find_by_sql(sql) - connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) } + def find_by_sql(sql, binds = []) + connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) } end # Creates an object (or multiple objects) and saves it to the database, if validations pass. @@ -504,12 +507,12 @@ module ActiveRecord #:nodoc: # Attributes listed as readonly will be used to create a new record but update operations will # ignore these fields. def attr_readonly(*attributes) - write_inheritable_attribute(:attr_readonly, Set.new(attributes.map { |a| a.to_s }) + (readonly_attributes || [])) + self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || []) end # Returns an array of all the attributes that have been specified as readonly. def readonly_attributes - read_inheritable_attribute(:attr_readonly) || [] + self._attr_readonly end # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, @@ -718,15 +721,12 @@ module ActiveRecord #:nodoc: # end # end def reset_column_information + connection.clear_cache! undefine_attribute_methods @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil @arel_engine = @relation = @arel_table = nil end - def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc: - descendants.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } - end - def attribute_method?(attribute) super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) end @@ -735,15 +735,12 @@ module ActiveRecord #:nodoc: def lookup_ancestors #:nodoc: klass = self classes = [klass] + return classes if klass == ActiveRecord::Base + while klass != klass.base_class classes << klass = klass.superclass end classes - rescue - # OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column' - # Apparently the method base_class causes some trouble. - # It now works for sure. - [self] end # Set the i18n scope to overwrite ActiveModel. @@ -1128,7 +1125,8 @@ MSG # Article.create.published # => true def default_scope(options = {}) reset_scoped_methods - self.default_scoping << construct_finder_arel(options, default_scoping.pop) + default_scoping = self.default_scoping.dup + self.default_scoping = default_scoping << construct_finder_arel(options, default_scoping.pop) end def current_scoped_methods #:nodoc: @@ -1285,7 +1283,7 @@ MSG # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" def sanitize_sql_array(ary) statement, *values = ary - if values.first.is_a?(Hash) and statement =~ /:\w+/ + if values.first.is_a?(Hash) && statement =~ /:\w+/ replace_named_bind_variables(statement, values.first) elsif statement.include?('?') replace_bind_variables(statement, values) @@ -1367,7 +1365,7 @@ MSG def initialize(attributes = nil) @attributes = attributes_from_column_definition @attributes_cache = {} - @new_record = true + @persisted = false @readonly = false @destroyed = false @marked_for_destruction = false @@ -1384,30 +1382,6 @@ MSG result end - # Cloned objects have no id assigned and are treated as new records. Note that this is a "shallow" clone - # 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) - _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - cloned_attributes.delete(self.class.primary_key) - - @attributes = cloned_attributes - - @changed_attributes = {} - attributes_from_column_definition.each do |attr, orig_value| - @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) - end - - clear_aggregation_cache - clear_association_cache - @attributes_cache = {} - @new_record = true - ensure_proper_type - - populate_with_current_scope_attributes - end - # Initialize an empty model object from +coder+. +coder+ must contain # the attributes necessary for initializing an empty model object. For # example: @@ -1421,7 +1395,8 @@ MSG def init_with(coder) @attributes = coder['attributes'] @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} - @new_record = @readonly = @destroyed = @marked_for_destruction = false + @readonly = @destroyed = @marked_for_destruction = false + @persisted = true _run_find_callbacks _run_initialize_callbacks end @@ -1462,7 +1437,7 @@ MSG # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) def cache_key case - when new_record? + when !persisted? "#{self.class.model_name.cache_key}/new" when timestamp = self[:updated_at] "#{self.class.model_name.cache_key}/#{id}-#{timestamp.to_s(:number)}" @@ -1571,8 +1546,7 @@ MSG # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). def attribute_present?(attribute) - value = read_attribute(attribute) - !value.blank? + !read_attribute(attribute).blank? end # Returns the column object for the named attribute. @@ -1580,16 +1554,25 @@ MSG self.class.columns_hash[name.to_s] end - # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id. + # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ + # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+. + # + # Note that new records are different from any other record by definition, unless the + # other record is the receiver itself. Besides, if you fetch existing records with + # +select+ and leave the ID out, you're on your own, this predicate will return false. + # + # Note also that destroying a record preserves its ID in the model instance, so deleted + # models are still comparable. def ==(comparison_object) comparison_object.equal?(self) || - (comparison_object.instance_of?(self.class) && - comparison_object.id == id && !comparison_object.new_record?) + comparison_object.instance_of?(self.class) && + id.present? && + comparison_object.id == id end # Delegates to == def eql?(comparison_object) - self == (comparison_object) + self == comparison_object end # Delegates to id in order to allow two records of the same type and id to work with something like: @@ -1608,11 +1591,42 @@ MSG @attributes.frozen? end - # Returns duplicated record with unfreezed attributes. - def dup - obj = super - obj.instance_variable_set('@attributes', @attributes.dup) - obj + # Backport dup from 1.9 so that initialize_dup() gets called + unless Object.respond_to?(:initialize_dup) + def dup # :nodoc: + copy = super + copy.initialize_dup(self) + copy + end + end + + # Duped objects have no id assigned and are treated as new records. Note + # that this is a "shallow" copy as it copies the object's attributes + # only, not its associations. The extent of a "deep" copy is application + # specific and is therefore left to the application to implement according + # to its need. + # The dup method does not preserve the timestamps (created|updated)_(at|on). + def initialize_dup(other) + cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) + cloned_attributes.delete(self.class.primary_key) + + @attributes = cloned_attributes + + _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) + + @changed_attributes = {} + attributes_from_column_definition.each do |attr, orig_value| + @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) + end + + clear_aggregation_cache + clear_association_cache + @attributes_cache = {} + @persisted = false + + ensure_proper_type + populate_with_current_scope_attributes + clear_timestamp_attributes end # Returns +true+ if the record is read only. Records loaded through joins with piggy-back @@ -1629,7 +1643,7 @@ MSG # Returns the contents of the record as a nicely formatted string. def inspect attributes_as_nice_string = self.class.column_names.collect { |name| - if has_attribute?(name) || new_record? + if has_attribute?(name) || !persisted? "#{name}: #{attribute_for_inspect(name)}" end }.compact.join(", ") @@ -1819,6 +1833,16 @@ MSG create_with.each { |att,value| self.respond_to?(:"#{att}=") && self.send("#{att}=", value) } if create_with end end + + # Clear attributes and changed_attributes + def clear_timestamp_attributes + %w(created_at created_on updated_at updated_on).each do |attribute_name| + if has_attribute?(attribute_name) + self[attribute_name] = nil + changed_attributes.delete(attribute_name) + end + end + end end Base.class_eval do 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 ca9314ec99..cffa2387de 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,3 +1,4 @@ +require 'thread' require 'monitor' require 'set' require 'active_support/core_ext/module/synchronization' 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 646a78622c..ee9a0af35c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -3,8 +3,16 @@ module ActiveRecord module DatabaseStatements # Returns an array of record hashes with the column names as keys and # column values as values. - def select_all(sql, name = nil) - select(sql, name) + def select_all(sql, name = nil, binds = []) + if supports_statement_cache? + select(sql, name, binds) + else + return select(sql, name) if binds.empty? + binds = binds.dup + select sql.gsub('?') { + quote(*binds.shift.reverse) + }, name + end end # Returns a record hash with the column names as keys and column values @@ -39,6 +47,12 @@ module ActiveRecord end undef_method :execute + # Executes +sql+ statement in the context of this connection using + # +binds+ as the bind substitutes. +name+ is logged along with + # the executed +sql+ statement. + def exec_query(sql, name = 'SQL', binds = []) + end + # Returns the last auto-generated ID from the affected table. def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) insert_sql(sql, name, pk, id_value, sequence_name) @@ -68,6 +82,12 @@ module ActiveRecord nil end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + false + end + # Runs the given block in a database transaction, and returns the result # of the block. # @@ -254,7 +274,7 @@ module ActiveRecord protected # Returns an array of record hashes with the column names as keys and # column values as values. - def select(sql, name = nil) + def select(sql, name = nil, binds = []) end undef_method :select diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 0ee61d0b6f..d555308485 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/duplicable' - module ActiveRecord module ConnectionAdapters # :nodoc: module QueryCache @@ -49,32 +47,26 @@ module ActiveRecord @query_cache.clear end - def select_all(*args) + def select_all(sql, name = nil, binds = []) if @query_cache_enabled - cache_sql(args.first) { super } + cache_sql(sql, binds) { super } else super end end private - def cache_sql(sql) + def cache_sql(sql, binds) result = - if @query_cache.has_key?(sql) + if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument("sql.active_record", :sql => sql, :name => "CACHE", :connection_id => self.object_id) - @query_cache[sql] + @query_cache[sql][binds] else - @query_cache[sql] = yield + @query_cache[sql][binds] = yield end - if Array === result - result.collect { |row| row.dup } - else - result.duplicable? ? result.dup : result - end - rescue TypeError - result + result.collect { |row| row.dup } end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index d8c92d0ad3..0282493219 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -12,6 +12,7 @@ require 'active_record/connection_adapters/abstract/connection_pool' require 'active_record/connection_adapters/abstract/connection_specification' require 'active_record/connection_adapters/abstract/query_cache' require 'active_record/connection_adapters/abstract/database_limits' +require 'active_record/result' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -40,7 +41,7 @@ module ActiveRecord @active = nil @connection, @logger = connection, logger @query_cache_enabled = false - @query_cache = {} + @query_cache = Hash.new { |h,sql| h[sql] = {} } @instrumenter = ActiveSupport::Notifications.instrumenter end @@ -97,6 +98,12 @@ module ActiveRecord quote_column_name(name) end + # Returns a bind substitution value given a +column+ and list of current + # +binds+ + def substitute_for(column, binds) + Arel.sql '?' + end + # REFERENTIAL INTEGRITY ==================================== # Override to turn off referential integrity while executing <tt>&block</tt>. @@ -135,6 +142,13 @@ module ActiveRecord # this should be overridden by concrete adapters end + ### + # Clear any caching the database adapter may be doing, for example + # clearing the prepared statement cache. This is database specific. + def clear_cache! + # this should be overridden by concrete adapters + end + # 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? @@ -190,8 +204,7 @@ module ActiveRecord protected - def log(sql, name) - name ||= "SQL" + def log(sql, name = "SQL") @instrumenter.instrument("sql.active_record", :sql => sql, :name => name, :connection_id => object_id) do yield diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index a4b336dfaf..ce2352486b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -3,6 +3,28 @@ require 'active_support/core_ext/kernel/requires' require 'active_support/core_ext/object/blank' require 'set' +begin + require 'mysql' +rescue LoadError + raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql'" +end + +unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash) + raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'" +end + +class Mysql + class Time + ### + # This monkey patch is for test_additional_columns_from_join_table + def to_date + Date.new(year, month, day) + end + end + class Stmt; include Enumerable end + class Result; include Enumerable end +end + module ActiveRecord class Base # Establishes a connection to the database that's used by all Active Record objects. @@ -15,18 +37,6 @@ module ActiveRecord password = config[:password].to_s database = config[:database] - unless defined? Mysql - begin - require 'mysql' - rescue LoadError - raise "!!! Missing the mysql2 gem. Add it to your Gemfile: gem 'mysql2'" - end - - unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash) - raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'" - end - end - mysql = Mysql.init mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey] @@ -39,6 +49,30 @@ module ActiveRecord module ConnectionAdapters class MysqlColumn < Column #:nodoc: + class << self + def string_to_time(value) + return super unless Mysql::Time === value + new_time( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.second_part) + end + + def string_to_dummy_time(v) + return super unless Mysql::Time === v + new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part) + end + + def string_to_date(v) + return super unless Mysql::Time === v + new_date(v.year, v.month, v.day) + end + end + def extract_default(default) if sql_type =~ /blob/i || type == :text if default.blank? @@ -161,6 +195,7 @@ module ActiveRecord super(connection, logger) @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} + @statements = {} connect end @@ -168,6 +203,12 @@ module ActiveRecord ADAPTER_NAME end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + def supports_migrations? #:nodoc: true end @@ -252,6 +293,7 @@ module ActiveRecord def reconnect! disconnect! + clear_cache! connect end @@ -272,14 +314,63 @@ module ActiveRecord def select_rows(sql, name = nil) @connection.query_with_result = true - result = execute(sql, name) - rows = [] - result.each { |row| rows << row } - result.free + rows = exec_without_stmt(sql, name).rows @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end + def clear_cache! + @statements.values.each do |cache| + cache[:stmt].close + end + @statements.clear + end + + def exec_query(sql, name = 'SQL', binds = []) + log(sql, name) do + result = nil + + cache = {} + if binds.empty? + stmt = @connection.prepare(sql) + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + end + + stmt.execute(*binds.map { |col, val| + col ? col.type_cast(val) : val + }) + if metadata = stmt.result_metadata + cols = cache[:cols] ||= metadata.fetch_fields.map { |field| + field.name + } + + metadata.free + result = ActiveRecord::Result.new(cols, stmt.to_a) + end + + stmt.free_result + stmt.close if binds.empty? + + result + end + end + + def exec_without_stmt(sql, name = 'SQL') # :nodoc: + # Some queries, like SHOW CREATE TABLE don't work through the prepared + # statement API. For those queries, we need to use this method. :'( + log(sql, name) do + result = @connection.query(sql) + cols = result.fetch_fields.map { |field| field.name } + rows = result.to_a + result.free + ActiveRecord::Result.new(cols, rows) + end + end + # Executes an SQL query and returns a MySQL::Result object. Note that you have to free # the Result object after you're done using it. def execute(sql, name = nil) #:nodoc: @@ -308,7 +399,7 @@ module ActiveRecord end def begin_db_transaction #:nodoc: - execute "BEGIN" + exec_without_stmt "BEGIN" rescue Exception # Transactions aren't supported end @@ -360,7 +451,8 @@ module ActiveRecord select_all(sql).map do |table| table.delete('Table_type') - select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n" + sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" + exec_without_stmt(sql).first['Create Table'] + ";\n\n" end.join("") end @@ -614,12 +706,9 @@ module ActiveRecord execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) end - def select(sql, name = nil) + def select(sql, name = nil, binds = []) @connection.query_with_result = true - result = execute(sql, name) - rows = [] - result.each_hash { |row| rows << row } - result.free + rows = exec_query(sql, name, binds).to_a @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5949985e4d..ccc5085b84 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -211,6 +211,12 @@ module ActiveRecord ADAPTER_NAME end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) @@ -220,11 +226,19 @@ module ActiveRecord @local_tz = nil @table_alias_length = nil @postgresql_version = nil + @statements = {} connect @local_tz = execute('SHOW TIME ZONE').first["TimeZone"] end + def clear_cache! + @statements.each_value do |value| + @connection.query "DEALLOCATE #{value}" + end + @statements.clear + end + # Is this connection alive and ready for queries? def active? if @connection.respond_to?(:status) @@ -242,6 +256,7 @@ module ActiveRecord # Close then reopen the connection. def reconnect! if @connection.respond_to?(:reset) + clear_cache! @connection.reset configure_connection else @@ -250,8 +265,14 @@ module ActiveRecord end end + def reset! + clear_cache! + super + end + # Close the connection. def disconnect! + clear_cache! @connection.close rescue nil end @@ -374,6 +395,12 @@ module ActiveRecord end end + # Set the authorized user for this session + def session_auth=(user) + clear_cache! + exec_query "SET SESSION AUTHORIZATION #{user}" + end + # REFERENTIAL INTEGRITY ==================================== def supports_disable_referential_integrity?() #:nodoc: @@ -501,6 +528,35 @@ module ActiveRecord end end + def substitute_for(column, current_values) + Arel.sql("$#{current_values.length + 1}") + end + + def exec_query(sql, name = 'SQL', binds = []) + return exec_no_cache(sql, name) if binds.empty? + + log(sql, name) do + unless @statements.key? sql + nextkey = "a#{@statements.length + 1}" + @connection.prepare nextkey, sql + @statements[sql] = nextkey + end + + key = @statements[sql] + + # Clear the queue + @connection.get_last_result + @connection.send_query_prepared(key, binds.map { |col, val| + col ? col.type_cast(val) : val + }) + @connection.block + result = @connection.get_last_result + ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) + result.clear + return ret + end + end + # Executes an UPDATE query and returns the number of affected tuples. def update_sql(sql, name = nil) super.cmd_tuples @@ -658,8 +714,8 @@ module ActiveRecord # Returns the list of all column definitions for a table. def columns(table_name, name = nil) # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).collect do |name, type, default, notnull| - PostgreSQLColumn.new(name, default, type, notnull == 'f') + column_definitions(table_name).collect do |column_name, type, default, notnull| + PostgreSQLColumn.new(column_name, default, type, notnull == 'f') end end @@ -876,12 +932,12 @@ module ActiveRecord # requires that the ORDER BY include the distinct column. # # distinct("posts.id", "posts.created_at desc") - def distinct(columns, order_by) #:nodoc: - return "DISTINCT #{columns}" if order_by.blank? + def distinct(columns, orders) #:nodoc: + return "DISTINCT #{columns}" if orders.empty? # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers - order_columns = order_by.split(',').collect { |s| s.split.first } + order_columns = orders.collect { |s| s =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : s } order_columns.delete_if { |c| c.blank? } order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } @@ -920,6 +976,15 @@ module ActiveRecord end private + def exec_no_cache(sql, name) + log(sql, name) do + result = @connection.async_exec(sql) + ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) + result.clear + ret + end + end + # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # The internal PostgreSQL identifier of the BYTEA data type. @@ -974,11 +1039,8 @@ module ActiveRecord # Executes a SELECT query and returns the results, performing any data type # conversions that are required to be performed here instead of in PostgreSQLColumn. - def select(sql, name = nil) - fields, rows = select_raw(sql, name) - rows.map do |row| - Hash[fields.zip(row)] - end + def select(sql, name = nil, binds = []) + exec_query(sql, name, binds).to_a end def select_raw(sql, name = nil) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 5ca1923d89..c2cd9e8d5e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -40,8 +40,7 @@ module ActiveRecord if @connection.respond_to?(:encoding) @connection.encoding.to_s else - encoding = @connection.execute('PRAGMA encoding') - encoding[0]['encoding'] + @connection.execute('PRAGMA 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 69bf2c0dcd..d76fc4103e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -50,6 +50,7 @@ module ActiveRecord def initialize(connection, logger, config) super(connection, logger) + @statements = {} @config = config end @@ -61,6 +62,12 @@ module ActiveRecord sqlite_version >= '2.0.0' end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + def supports_migrations? #:nodoc: true end @@ -79,9 +86,14 @@ module ActiveRecord def disconnect! super + clear_cache! @connection.close rescue nil end + def clear_cache! + @statements.clear + end + def supports_count_distinct? #:nodoc: sqlite_version >= '3.2.6' end @@ -131,6 +143,29 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def exec_query(sql, name = nil, binds = []) + log(sql, name) do + + # Don't cache statements without bind values + if binds.empty? + stmt = @connection.prepare(sql) + cols = stmt.columns + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params binds.map { |col, val| + col ? col.type_cast(val) : val + } + end + + ActiveRecord::Result.new(cols, stmt.to_a) + end + end + def execute(sql, name = nil) #:nodoc: log(sql, name) { @connection.execute(sql) } end @@ -151,9 +186,7 @@ module ActiveRecord alias :create :insert_sql def select_rows(sql, name = nil) - execute(sql, name).map do |row| - (0...(row.size / 2)).map { |i| row[i] } - end + exec_query(sql, name).rows end def begin_db_transaction #:nodoc: @@ -177,7 +210,7 @@ module ActiveRecord WHERE type = 'table' AND NOT name = 'sqlite_sequence' SQL - execute(sql, name).map do |row| + exec_query(sql, name).map do |row| row['name'] end end @@ -189,12 +222,12 @@ module ActiveRecord end def indexes(table_name, name = nil) #:nodoc: - execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| IndexDefinition.new( table_name, row['name'], - row['unique'].to_i != 0, - execute("PRAGMA index_info('#{row['name']}')").map { |col| + row['unique'] != 0, + exec_query("PRAGMA index_info('#{row['name']}')").map { |col| col['name'] }) end @@ -202,17 +235,17 @@ module ActiveRecord def primary_key(table_name) #:nodoc: column = table_structure(table_name).find { |field| - field['pk'].to_i == 1 + field['pk'] == 1 } column && column['name'] end def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_column_name(index_name)}" + exec_query "DROP INDEX #{quote_column_name(index_name)}" end def rename_table(name, new_name) - execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" end # See: http://www.sqlite.org/lang_altertable.html @@ -249,7 +282,7 @@ module ActiveRecord def change_column_null(table_name, column_name, null, default = nil) unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end alter_table(table_name) do |definition| definition[column_name].null = null @@ -280,14 +313,13 @@ module ActiveRecord end protected - def select(sql, name = nil) #:nodoc: - execute(sql, name).map do |row| - record = {} - row.each do |key, value| - record[key.sub(/^"?\w+"?\./, '')] = value if key.is_a?(String) - end - record - end + def select(sql, name = nil, binds = []) #:nodoc: + result = exec_query(sql, name, binds) + columns = result.columns.map { |column| + column.sub(/^"?\w+"?\./, '') + } + + result.rows.map { |row| Hash[columns.zip(row)] } end def table_structure(table_name) @@ -367,11 +399,11 @@ module ActiveRecord quoted_columns = columns.map { |col| quote_column_name(col) } * ',' quoted_to = quote_table_name(to) - @connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row| + exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' sql << ')' - @connection.execute sql + exec_query sql end end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index b6f87a57b8..c0e1dda2bd 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -70,7 +70,7 @@ module ActiveRecord result[self.class.locking_column] ||= 0 end - return result + result end def update(attribute_names = @attributes.keys) #:nodoc: @@ -89,7 +89,7 @@ module ActiveRecord affected_rows = relation.where( relation.table[self.class.primary_key].eq(quoted_id).and( - relation.table[self.class.locking_column].eq(quote_value(previous_value)) + relation.table[lock_col].eq(quote_value(previous_value)) ) ).arel.update(arel_attributes_values(false, false, attribute_names)) @@ -109,13 +109,11 @@ module ActiveRecord def destroy #:nodoc: return super unless locking_enabled? - unless new_record? - lock_col = self.class.locking_column - previous_value = send(lock_col).to_i - + if persisted? table = self.class.arel_table - predicate = table[self.class.primary_key].eq(id) - predicate = predicate.and(table[self.class.locking_column].eq(previous_value)) + lock_col = self.class.locking_column + predicate = table[self.class.primary_key].eq(id). + and(table[lock_col].eq(send(lock_col).to_i)) affected_rows = self.class.unscoped.where(predicate).delete_all diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 9ad6a2baf7..d900831e13 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -47,7 +47,7 @@ module ActiveRecord # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns # the locked record. def lock!(lock = true) - reload(:lock => lock) unless new_record? + reload(:lock => lock) if persisted? self end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index a4c09b654a..f6321f1499 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,6 +1,3 @@ -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. class IrreversibleMigration < ActiveRecordError @@ -43,11 +40,11 @@ module ActiveRecord # Example of a simple migration: # # class AddSsl < ActiveRecord::Migration - # def self.up + # def up # add_column :accounts, :ssl_enabled, :boolean, :default => 1 # end # - # def self.down + # def down # remove_column :accounts, :ssl_enabled # end # end @@ -63,7 +60,7 @@ module ActiveRecord # Example of a more complex migration that also needs to initialize data: # # class AddSystemSettings < ActiveRecord::Migration - # def self.up + # def up # create_table :system_settings do |t| # t.string :name # t.string :label @@ -77,7 +74,7 @@ module ActiveRecord # :value => 1 # end # - # def self.down + # def down # drop_table :system_settings # end # end @@ -138,7 +135,7 @@ module ActiveRecord # 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 + # You may then edit the <tt>up</tt> and <tt>down</tt> methods of # MyNewMigration. # # There is a special syntactic shortcut to generate migrations that add fields to a table. @@ -147,11 +144,11 @@ module ActiveRecord # # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: # class AddFieldnameToTablename < ActiveRecord::Migration - # def self.up + # def up # add_column :tablenames, :fieldname, :string # end # - # def self.down + # def down # remove_column :tablenames, :fieldname # end # end @@ -179,11 +176,11 @@ module ActiveRecord # Not all migrations change the schema. Some just fix the data: # # class RemoveEmptyTags < ActiveRecord::Migration - # def self.up + # def up # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? } # end # - # def self.down + # def down # # not much we can do to restore deleted data # raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags" # end @@ -192,12 +189,12 @@ module ActiveRecord # Others remove columns when they migrate up instead of down: # # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration - # def self.up + # def up # remove_column :items, :incomplete_items_count # remove_column :items, :completed_items_count # end # - # def self.down + # def down # add_column :items, :incomplete_items_count # add_column :items, :completed_items_count # end @@ -206,11 +203,11 @@ module ActiveRecord # And sometimes you need to do something in SQL not abstracted directly by migrations: # # class MakeJoinUnique < ActiveRecord::Migration - # def self.up + # def up # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)" # end # - # def self.down + # def down # execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`" # end # end @@ -223,7 +220,7 @@ module ActiveRecord # latest column data from after the new column was added. Example: # # class AddPeopleSalary < ActiveRecord::Migration - # def self.up + # def up # add_column :people, :salary, :integer # Person.reset_column_information # Person.find(:all).each do |p| @@ -243,7 +240,7 @@ module ActiveRecord # You can also insert your own messages and benchmarks by using the +say_with_time+ # method: # - # def self.up + # def up # ... # say_with_time "Updating salaries..." do # Person.find(:all).each do |p| @@ -286,143 +283,201 @@ module ActiveRecord # # In application.rb. # + # == Reversible Migrations + # + # Starting with Rails 3.1, you will be able to define reversible migrations. + # Reversible migrations are migrations that know how to go +down+ for you. + # You simply supply the +up+ logic, and the Migration system will figure out + # how to execute the down commands for you. + # + # To define a reversible migration, define the +change+ method in your + # migration like this: + # + # class TenderloveMigration < ActiveRecord::Migration + # def change + # create_table(:horses) do + # t.column :content, :text + # t.column :remind_at, :datetime + # end + # end + # end + # + # This migration will create the horses table for you on the way up, and + # automatically figure out how to drop the table on the way down. + # + # Some commands like +remove_column+ cannot be reversed. If you care to + # define how to move up and down in these cases, you should define the +up+ + # and +down+ methods as before. + # + # If a command cannot be reversed, an + # <tt>ActiveRecord::IrreversibleMigration</tt> exception will be raised when + # the migration is moving down. + # + # For a list of commands that are reversible, please see + # <tt>ActiveRecord::Migration::CommandRecorder</tt>. class Migration - @@verbose = true - cattr_accessor :verbose + autoload :CommandRecorder, 'active_record/migration/command_recorder' class << self - def up_with_benchmarks #:nodoc: - migrate(:up) - end + attr_accessor :delegate # :nodoc: + end - def down_with_benchmarks #:nodoc: - migrate(:down) - end + def self.method_missing(name, *args, &block) # :nodoc: + (delegate || superclass.delegate).send(name, *args, &block) + end - # Execute this migration in the named direction - def migrate(direction) - return unless respond_to?(direction) + cattr_accessor :verbose - case direction - when :up then announce "migrating" - when :down then announce "reverting" - end + attr_accessor :name, :version - result = nil - time = Benchmark.measure { result = send("#{direction}_without_benchmarks") } + def initialize + @name = self.class.name + @version = nil + @connection = nil + end - case direction - when :up then announce "migrated (%.4fs)" % time.real; write - when :down then announce "reverted (%.4fs)" % time.real; write - end + # instantiate the delegate object after initialize is defined + self.verbose = true + self.delegate = new - result - end + def up + self.class.delegate = self + return unless self.class.respond_to?(:up) + self.class.up + end + + def down + self.class.delegate = self + return unless self.class.respond_to?(:down) + self.class.down + end - # Because the method added may do an alias_method, it can be invoked - # recursively. We use @ignore_new_methods as a guard to indicate whether - # it is safe for the call to proceed. - def singleton_method_added(sym) #:nodoc: - return if defined?(@ignore_new_methods) && @ignore_new_methods + # Execute this migration in the named direction + def migrate(direction) + return unless respond_to?(direction) - begin - @ignore_new_methods = true + case direction + when :up then announce "migrating" + when :down then announce "reverting" + end - case sym - when :up, :down - singleton_class.send(:alias_method_chain, sym, "benchmarks") + time = nil + ActiveRecord::Base.connection_pool.with_connection do |conn| + @connection = conn + if respond_to?(:change) + if direction == :down + recorder = CommandRecorder.new(@connection) + suppress_messages do + @connection = recorder + change + end + @connection = conn + time = Benchmark.measure { + recorder.inverse.each do |cmd, args| + send(cmd, *args) + end + } + else + time = Benchmark.measure { change } end - ensure - @ignore_new_methods = false + else + time = Benchmark.measure { send(direction) } end + @connection = nil end - def write(text="") - puts(text) if verbose + case direction + when :up then announce "migrated (%.4fs)" % time.real; write + when :down then announce "reverted (%.4fs)" % time.real; write end + end - def announce(message) - version = defined?(@version) ? @version : nil + def write(text="") + puts(text) if verbose + end - text = "#{version} #{name}: #{message}" - length = [0, 75 - text.length].max - write "== %s %s" % [text, "=" * length] - end + def announce(message) + text = "#{version} #{name}: #{message}" + length = [0, 75 - text.length].max + write "== %s %s" % [text, "=" * length] + end - def say(message, subitem=false) - write "#{subitem ? " ->" : "--"} #{message}" - end + def say(message, subitem=false) + write "#{subitem ? " ->" : "--"} #{message}" + end - def say_with_time(message) - say(message) - result = nil - time = Benchmark.measure { result = yield } - say "%.4fs" % time.real, :subitem - say("#{result} rows", :subitem) if result.is_a?(Integer) - result - end + def say_with_time(message) + say(message) + result = nil + time = Benchmark.measure { result = yield } + say "%.4fs" % time.real, :subitem + say("#{result} rows", :subitem) if result.is_a?(Integer) + result + end - def suppress_messages - save, self.verbose = verbose, false - yield - ensure - self.verbose = save - end + def suppress_messages + save, self.verbose = verbose, false + yield + ensure + self.verbose = save + end - def connection - ActiveRecord::Base.connection - end + def connection + @connection || ActiveRecord::Base.connection + end - def method_missing(method, *arguments, &block) - arg_list = arguments.map{ |a| a.inspect } * ', ' + def method_missing(method, *arguments, &block) + arg_list = arguments.map{ |a| a.inspect } * ', ' - say_with_time "#{method}(#{arg_list})" do - unless arguments.empty? || method == :execute - arguments[0] = Migrator.proper_table_name(arguments.first) - end - connection.send(method, *arguments, &block) + say_with_time "#{method}(#{arg_list})" do + unless arguments.empty? || method == :execute + arguments[0] = Migrator.proper_table_name(arguments.first) end + return super unless connection.respond_to?(method) + connection.send(method, *arguments, &block) end + end - def copy(destination, sources, options = {}) - copied = [] - - destination_migrations = ActiveRecord::Migrator.migrations(destination) - last = destination_migrations.last - sources.each do |name, path| - source_migrations = ActiveRecord::Migrator.migrations(path) + def copy(destination, sources, options = {}) + copied = [] - source_migrations.each do |migration| - source = File.read(migration.filename) - source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}" + FileUtils.mkdir_p(destination) unless File.exists?(destination) - if duplicate = destination_migrations.detect { |m| m.name == migration.name } - options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip] - next - end + destination_migrations = ActiveRecord::Migrator.migrations(destination) + last = destination_migrations.last + sources.each do |name, path| + source_migrations = ActiveRecord::Migrator.migrations(path) - migration.version = next_migration_number(last ? last.version + 1 : 0).to_i - new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb") - old_path, migration.filename = migration.filename, new_path - last = migration + source_migrations.each do |migration| + source = File.read(migration.filename) + source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}" - FileUtils.cp(old_path, migration.filename) - copied << migration - options[:on_copy].call(name, migration, old_path) if options[:on_copy] - destination_migrations << migration + if duplicate = destination_migrations.detect { |m| m.name == migration.name } + options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip] + next end - end - copied - end + migration.version = next_migration_number(last ? last.version + 1 : 0).to_i + new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb") + old_path, migration.filename = migration.filename, new_path + last = migration - def next_migration_number(number) - if ActiveRecord::Base.timestamped_migrations - [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max - else - "%.3d" % number + FileUtils.cp(old_path, migration.filename) + copied << migration + options[:on_copy].call(name, migration, old_path) if options[:on_copy] + destination_migrations << migration end end + + copied + end + + def next_migration_number(number) + if ActiveRecord::Base.timestamped_migrations + [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max + else + "%.3d" % number + end end end @@ -449,7 +504,7 @@ module ActiveRecord def load_migration require(File.expand_path(filename)) - name.constantize + name.constantize.new end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb new file mode 100644 index 0000000000..d7e481905a --- /dev/null +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -0,0 +1,91 @@ +module ActiveRecord + class Migration + # ActiveRecord::Migration::CommandRecorder records commands done during + # a migration and knows how to reverse those commands. The CommandRecorder + # knows how to invert the following commands: + # + # * add_column + # * add_index + # * add_timestamp + # * create_table + # * remove_timestamps + # * rename_column + # * rename_index + # * rename_table + class CommandRecorder + attr_accessor :commands, :delegate + + def initialize(delegate = nil) + @commands = [] + @delegate = delegate + end + + # record +command+. +command+ should be a method name and arguments. + # For example: + # + # recorder.record(:method_name, [:arg1, arg2]) + def record(*command) + @commands << command + end + + # Returns a list that represents commands that are the inverse of the + # commands stored in +commands+. For example: + # + # recorder.record(:rename_table, [:old, :new]) + # recorder.inverse # => [:rename_table, [:new, :old]] + # + # This method will raise an IrreversibleMigration exception if it cannot + # invert the +commands+. + def inverse + @commands.reverse.map { |name, args| + method = :"invert_#{name}" + raise IrreversibleMigration unless respond_to?(method, true) + __send__(method, args) + } + end + + def respond_to?(*args) # :nodoc: + super || delegate.respond_to?(*args) + end + + def send(method, *args) # :nodoc: + return super unless respond_to?(method) + record(method, args) + end + + private + def invert_create_table(args) + [:drop_table, args] + end + + def invert_rename_table(args) + [:rename_table, args.reverse] + end + + def invert_add_column(args) + [:remove_column, args.first(2)] + end + + def invert_rename_index(args) + [:rename_index, args.reverse] + end + + def invert_rename_column(args) + [:rename_column, [args.first] + args.last(2).reverse] + end + + def invert_add_index(args) + table, columns, _ = *args + [:remove_index, [table, {:column => columns}]] + end + + def invert_remove_timestamps(args) + [:add_timestamps, args] + end + + def invert_add_timestamps(args) + [:remove_timestamps, args] + end + end + end +end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 0b92ba5caa..0f421560f0 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -2,12 +2,18 @@ require 'active_support/core_ext/array' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/class/attribute' module ActiveRecord # = Active Record Named \Scopes module NamedScope extend ActiveSupport::Concern + included do + class_attribute :scopes + self.scopes = {} + end + module ClassMethods # Returns an anonymous \scope. # @@ -33,10 +39,6 @@ module ActiveRecord end end - def scopes - 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, # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>. # diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index aca91c907d..050b521b6a 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/class/attribute' module ActiveRecord module NestedAttributes #:nodoc: @@ -11,7 +12,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_inheritable_accessor :nested_attributes_options, :instance_writer => false + class_attribute :nested_attributes_options, :instance_writer => false self.nested_attributes_options = {} end @@ -268,7 +269,11 @@ module ActiveRecord if reflection = reflect_on_association(association_name) reflection.options[:autosave] = true add_autosave_association_callbacks(reflection) + + nested_attributes_options = self.nested_attributes_options.dup nested_attributes_options[association_name.to_sym] = options + self.nested_attributes_options = nested_attributes_options + type = (reflection.collection? ? :collection : :one_to_one) # def pirate_attributes=(attributes) @@ -315,15 +320,14 @@ module ActiveRecord # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) - options = nested_attributes_options[association_name] + options = self.nested_attributes_options[association_name] attributes = attributes.with_indifferent_access - check_existing_record = (options[:update_only] || !attributes['id'].blank?) - if check_existing_record && (record = send(association_name)) && + if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) && (options[:update_only] || record.id.to_s == attributes['id'].to_s) assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) - elsif attributes['id'] + elsif attributes['id'].present? raise_nested_attributes_record_not_found(association_name, attributes['id']) elsif !reject_new_record?(association_name, attributes) @@ -364,7 +368,7 @@ module ActiveRecord # { :id => '2', :_destroy => true } # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) - options = nested_attributes_options[association_name] + options = self.nested_attributes_options[association_name] unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" @@ -413,11 +417,8 @@ module ActiveRecord # Updates a record with the +attributes+ or marks it for destruction if # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) - if has_destroy_flag?(attributes) && allow_destroy - record.mark_for_destruction - else - record.attributes = attributes.except(*UNASSIGNABLE_KEYS) - end + record.attributes = attributes.except(*UNASSIGNABLE_KEYS) + record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy end # Determines if a hash contains a truthy _destroy key. @@ -433,7 +434,7 @@ module ActiveRecord end def call_reject_if(association_name, attributes) - case callback = nested_attributes_options[association_name][:reject_if] + case callback = self.nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) when Proc @@ -442,8 +443,7 @@ module ActiveRecord 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}" + raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 707c1a05be..594a2214bb 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -4,7 +4,7 @@ module ActiveRecord # 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 + !@persisted end # Returns true if this object has been destroyed, otherwise returns false. @@ -15,7 +15,7 @@ module ActiveRecord # Returns if the record is persisted, i.e. it's not a new record and it was # not destroyed. def persisted? - !(new_record? || destroyed?) + @persisted && !destroyed? end # Saves the model. @@ -94,8 +94,9 @@ module ActiveRecord became = klass.new became.instance_variable_set("@attributes", @attributes) became.instance_variable_set("@attributes_cache", @attributes_cache) - became.instance_variable_set("@new_record", new_record?) + became.instance_variable_set("@persisted", persisted?) became.instance_variable_set("@destroyed", destroyed?) + became.type = klass.name unless self.class.descends_from_active_record? became end @@ -240,7 +241,7 @@ module ActiveRecord private def create_or_update raise ReadOnlyRecord if readonly? - result = new_record? ? create : update + result = persisted? ? update : create result != false end @@ -269,7 +270,7 @@ module ActiveRecord self.id ||= new_id - @new_record = false + @persisted = true id end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 868fd6c3ff..dfe255ad7c 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -14,7 +14,7 @@ module ActiveRecord config.active_record = ActiveSupport::OrderedOptions.new config.app_generators.orm :active_record, :migration => true, - :timestamps => true + :timestamps => true config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::QueryCache" diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a2260e9a19..a07c321960 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,8 +1,15 @@ +require 'active_support/core_ext/class/attribute' + module ActiveRecord # = Active Record Reflection module Reflection # :nodoc: extend ActiveSupport::Concern + included do + class_attribute :reflections + self.reflections = {} + end + # Reflection enables to interrogate Active Record classes and objects # about their associations and aggregations. This information can, # for example, be used in a form builder that takes an Active Record object @@ -20,18 +27,9 @@ module ActiveRecord when :composed_of reflection = AggregateReflection.new(macro, name, options, active_record) end - write_inheritable_hash :reflections, name => reflection - reflection - end - # Returns a hash containing all AssociationReflection objects for the current class. - # Example: - # - # Invoice.reflections - # Account.reflections - # - def reflections - read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {}) + self.reflections = self.reflections.merge(name => reflection) + reflection end # Returns an array of AggregateReflection objects for all the aggregations in the class. diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index f129b54f9a..3b22be78cb 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -5,7 +5,7 @@ module ActiveRecord class Relation JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] - MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having] + MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from] include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches @@ -61,7 +61,7 @@ module ActiveRecord def to_a return @records if loaded? - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) preload = @preload_values preload += @includes_values unless eager_loading? @@ -319,8 +319,13 @@ module ActiveRecord end def where_values_hash - Hash[@where_values.find_all {|w| w.respond_to?(:operator) && w.operator == :== }.map {|where| - [where.operand1.name, where.operand2.respond_to?(:value) ? where.operand2.value : where.operand2] + Hash[@where_values.find_all { |w| + w.respond_to?(:operator) && w.operator == :== && w.left.relation.name == table_name + }.map { |where| + [ + where.left.name, + where.right.respond_to?(:value) ? where.right.value : where.right + ] }] end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 6bf698fe97..c8adaddfca 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -208,14 +208,16 @@ module ActiveRecord end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: - group_attr = @group_values.first - association = @klass.reflect_on_association(group_attr.to_sym) - associated = association && association.macro == :belongs_to # only count belongs_to associations - group_field = associated ? association.primary_key_name : group_attr - group_alias = column_alias_for(group_field) - group_column = column_for(group_field) + group_attr = @group_values + association = @klass.reflect_on_association(group_attr.first.to_sym) + associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + group_fields = Array(associated ? association.primary_key_name : group_attr) + group_aliases = group_fields.map { |field| column_alias_for(field) } + group_columns = group_aliases.zip(group_fields).map { |aliaz,field| + [aliaz, column_for(field)] + } - group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field + group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields if operation == 'count' && column_name == :all aggregate_alias = 'count_all' @@ -223,22 +225,33 @@ module ActiveRecord aggregate_alias = column_alias_for(operation, column_name) end - relation = except(:group).group(group) - relation.select_values = [ - operation_over_aggregate_column(aggregate_column(column_name), operation, distinct).as(aggregate_alias), - "#{group_field} AS #{group_alias}" + select_values = [ + operation_over_aggregate_column( + aggregate_column(column_name), + operation, + distinct).as(aggregate_alias) ] + select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| + "#{field} AS #{aliaz}" + } + + relation = except(:group).group(group.join(',')) + relation.select_values = select_values + calculated_data = @klass.connection.select_all(relation.to_sql) if association - key_ids = calculated_data.collect { |row| row[group_alias] } + key_ids = calculated_data.collect { |row| row[group_aliases.first] } key_records = association.klass.base_class.find(key_ids) key_records = Hash[key_records.map { |r| [r.id, r] }] end ActiveSupport::OrderedHash[calculated_data.map do |row| - key = type_cast_calculated_value(row[group_alias], group_column) + key = group_columns.map { |aliaz, column| + type_cast_calculated_value(row[aliaz], column) + } + key = key.first if key.size == 1 key = key_records[key] if associated [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)] end] diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index b763e22ec6..23ae0b4325 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -171,14 +171,16 @@ module ActiveRecord def exists?(id = nil) id = id.id if ActiveRecord::Base === id + relation = select(primary_key).limit(1) + case id when Array, Hash - where(id).exists? + relation = relation.where(id) else - relation = select(primary_key).limit(1) relation = relation.where(primary_key.eq(id)) if id - relation.first ? true : false end + + relation.first ? true : false end protected @@ -200,7 +202,7 @@ module ActiveRecord end def construct_relation_for_association_find(join_dependency) - relation = except(:includes, :eager_load, :preload, :select).select(column_aliases(join_dependency)) + relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns) apply_join_dependency(relation, join_dependency) end @@ -222,7 +224,7 @@ module ActiveRecord end def construct_limited_ids_condition(relation) - orders = relation.order_values.join(", ") + orders = relation.order_values values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders) ids_array = relation.select(values).collect {|row| row[@klass.primary_key]} @@ -288,12 +290,17 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - record = where(primary_key.eq(id)).first + column = primary_key.column + + substitute = connection.substitute_for(column, @bind_values) + relation = where(primary_key.eq(substitute)) + relation.bind_values = [[column, id]] + record = relation.first unless record conditions = arel.where_sql conditions = " [#{conditions}]" if conditions - raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}" + raise RecordNotFound, "Couldn't find #{@klass.name} with #{@klass.primary_key}=#{id}#{conditions}" end record @@ -342,17 +349,8 @@ module ActiveRecord end end - def column_aliases(join_dependency) - join_dependency.join_parts.collect { |join_part| - join_part.column_names_with_alias.collect{ |column_name, aliased_name| - "#{connection.quote_table_name join_part.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}" - } - }.flatten.join(", ") - end - def using_limitable_reflections?(reflections) reflections.none? { |r| r.collection? } end - end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index c5428dccd6..70d84619a1 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -25,6 +25,11 @@ module ActiveRecord attribute.in(values) when Range, Arel::Relation attribute.in(value) + when ActiveRecord::Base + attribute.eq(Arel.sql(value.quoted_id)) + when Class + # FIXME: I think we need to deprecate this behavior + attribute.eq(value.name) else attribute.eq(value) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 64d2cb0203..0a4c119849 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -6,13 +6,14 @@ module ActiveRecord extend ActiveSupport::Concern attr_accessor :includes_values, :eager_load_values, :preload_values, - :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, + :select_values, :group_values, :order_values, :joins_values, + :where_values, :having_values, :bind_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value def includes(*args) args.reject! {|a| a.blank? } - return clone if args.empty? + return self if args.empty? relation = clone relation.includes_values = (relation.includes_values + args).flatten.uniq @@ -20,14 +21,18 @@ module ActiveRecord end def eager_load(*args) + return self if args.blank? + relation = clone - relation.eager_load_values += args unless args.blank? + relation.eager_load_values += args relation end def preload(*args) + return self if args.blank? + relation = clone - relation.preload_values += args unless args.blank? + relation.preload_values += args relation end @@ -42,35 +47,51 @@ module ActiveRecord end def group(*args) + return self if args.blank? + relation = clone - relation.group_values += args.flatten unless args.blank? + relation.group_values += args.flatten relation end def order(*args) + return self if args.blank? + relation = clone - relation.order_values += args.flatten unless args.blank? + relation.order_values += args.flatten relation end def joins(*args) + return self if args.blank? + relation = clone args.flatten! - relation.joins_values += args unless args.blank? + relation.joins_values += args relation end + def bind(value) + relation = clone + relation.bind_values += [value] + relation + end + def where(opts, *rest) + return self if opts.blank? + relation = clone - relation.where_values += build_where(opts, rest) unless opts.blank? + relation.where_values += build_where(opts, rest) relation end def having(*args) + return self if args.blank? + relation = clone - relation.having_values += build_where(*args) unless args.blank? + relation.having_values += build_where(*args) relation end @@ -134,7 +155,7 @@ module ActiveRecord "#{@klass.table_name}.#{@klass.primary_key} DESC" : reverse_sql_order(order_clause).join(', ') - except(:order).order(Arel::SqlLiteral.new(order)) + except(:order).order(Arel.sql(order)) end def arel @@ -167,10 +188,7 @@ module ActiveRecord arel = build_joins(arel, @joins_values) unless @joins_values.empty? - (@where_values - ['']).uniq.each do |where| - where = Arel.sql(where) if String === where - arel = arel.where(Arel::Nodes::Grouping.new(where)) - end + arel = collapse_wheres(arel, (@where_values - ['']).uniq) arel = arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty? @@ -191,6 +209,27 @@ module ActiveRecord private + def collapse_wheres(arel, wheres) + equalities = wheres.grep(Arel::Nodes::Equality) + + groups = equalities.group_by do |equality| + equality.left + end + + groups.each do |_, eqls| + test = eqls.inject(eqls.shift) do |memo, expr| + memo.or(expr) + end + arel = arel.where(test) + end + + (wheres - equalities).each do |where| + where = Arel.sql(where) if String === where + arel = arel.where(Arel::Nodes::Grouping.new(where)) + end + arel + end + def build_where(opts, other = []) case opts when String, Array diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 648a02f1cc..a61a3bd41c 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -28,17 +28,20 @@ module ActiveRecord merged_wheres = @where_values + r.where_values - # Remove duplicates, last one wins. - seen = {} - merged_wheres = merged_wheres.reverse.reject { |w| - nuke = false - if w.respond_to?(:operator) && w.operator == :== - name = w.left.name - nuke = seen[name] - seen[name] = true - end - nuke - }.reverse + unless @where_values.empty? + # Remove duplicates, last one wins. + seen = Hash.new { |h,table| h[table] = {} } + merged_wheres = merged_wheres.reverse.reject { |w| + nuke = false + if w.respond_to?(:operator) && w.operator == :== + name = w.left.name + table = w.left.relation.name + nuke = seen[table][name] + seen[table][name] = true + end + nuke + }.reverse + end merged_relation.where_values = merged_wheres diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb new file mode 100644 index 0000000000..8deff1478f --- /dev/null +++ b/activerecord/lib/active_record/result.rb @@ -0,0 +1,30 @@ +module ActiveRecord + ### + # This class encapsulates a Result returned from calling +exec+ on any + # database connection adapter. For example: + # + # x = ActiveRecord::Base.connection.exec('SELECT * FROM foo') + # x # => #<ActiveRecord::Result:0xdeadbeef> + class Result + include Enumerable + + attr_reader :columns, :rows + + def initialize(columns, rows) + @columns = columns + @rows = rows + @hash_rows = nil + end + + def each + hash_rows.each { |row| yield row } + end + + private + def hash_rows + @hash_rows ||= @rows.map { |row| + ActiveSupport::OrderedHash[@columns.zip(row)] + } + end + end +end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index c1bc3214ea..c6bb5c1961 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -30,9 +30,7 @@ module ActiveRecord # ActiveRecord::Schema is only supported by database adapters that also # support migrations, the two features being very similar. class Schema < Migration - private_class_method :new - - def self.migrations_path + def migrations_path ActiveRecord::Migrator.migrations_path end @@ -48,11 +46,12 @@ module ActiveRecord # ... # end def self.define(info={}, &block) - instance_eval(&block) + schema = new + schema.instance_eval(&block) unless info[:version].blank? initialize_schema_migrations_table - assume_migrated_upto_version(info[:version], migrations_path) + assume_migrated_upto_version(info[:version], schema.migrations_path) end end end diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index 3fc596e02a..ba99800fb2 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -228,7 +228,7 @@ module ActiveRecord @session_id = attributes[:session_id] @data = attributes[:data] @marshaled_data = attributes[:marshaled_data] - @new_record = @marshaled_data.nil? + @persisted = !@marshaled_data.nil? end # Lazy-unmarshal session state. @@ -252,8 +252,8 @@ module ActiveRecord marshaled_data = self.class.marshal(data) connect = connection - if @new_record - @new_record = false + unless @persisted + @persisted = true connect.update <<-end_sql, 'Create session' INSERT INTO #{table_name} ( #{connect.quote_column_name(session_id_column)}, @@ -272,7 +272,7 @@ module ActiveRecord end def destroy - return if @new_record + return unless @persisted connect = connection connect.delete <<-end_sql, 'Destroy session' @@ -321,6 +321,7 @@ module ActiveRecord if sid = current_session_id(env) Base.silence do get_session_model(env, sid).destroy + env[SESSION_RECORD_KEY] = nil end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index a7583f06cc..2ecbd906bd 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/class/attribute' + module ActiveRecord # = Active Record Timestamp # @@ -29,14 +31,14 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_inheritable_accessor :record_timestamps, :instance_writer => false + class_attribute :record_timestamps, :instance_writer => false self.record_timestamps = true end private def create #:nodoc: - if record_timestamps + if self.record_timestamps current_time = current_time_from_proper_timezone all_timestamp_attributes.each do |column| @@ -61,7 +63,7 @@ module ActiveRecord end def should_record_timestamps? - record_timestamps && (!partial_updates? || changed?) + self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?) end def timestamp_attributes_for_update_in_model diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index ab737f0f88..8c94d1a2bc 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -242,7 +242,7 @@ module ActiveRecord with_transaction_returning_status { super } end - # Reset id and @new_record if the transaction rolls back. + # Reset id and @persisted if the transaction rolls back. def rollback_active_record_state! remember_transaction_record_state yield @@ -297,9 +297,9 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc @_start_transaction_state ||= {} - unless @_start_transaction_state.include?(:new_record) + unless @_start_transaction_state.include?(:persisted) @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) - @_start_transaction_state[:new_record] = @new_record + @_start_transaction_state[:persisted] = @persisted end unless @_start_transaction_state.include?(:destroyed) @_start_transaction_state[:destroyed] = @destroyed @@ -323,7 +323,7 @@ module ActiveRecord restore_state = remove_instance_variable(:@_start_transaction_state) if restore_state @attributes = @attributes.dup if @attributes.frozen? - @new_record = restore_state[:new_record] + @persisted = restore_state[:persisted] @destroyed = restore_state[:destroyed] if restore_state[:id] self.id = restore_state[:id] @@ -345,11 +345,11 @@ module ActiveRecord def transaction_include_action?(action) #:nodoc case action when :create - transaction_record_state(:new_record) + transaction_record_state(:new_record) || !transaction_record_state(:persisted) when :destroy destroyed? when :update - !(transaction_record_state(:new_record) || destroyed?) + !(transaction_record_state(:new_record) || !transaction_record_state(:persisted) || destroyed?) end end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index f367315b22..ee45fcdf35 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -51,7 +51,7 @@ 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) + context ||= (persisted? ? :update : :create) output = super(context) errors.empty? && output end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index cb1d2ae421..853808eebf 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -14,24 +14,21 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) - table = finder_class.unscoped - - table_name = record.class.quoted_table_name if value && record.class.serialized_attributes.key?(attribute.to_s) value = YAML.dump value end - sql, params = mount_sql_and_params(finder_class, table_name, attribute, value) + sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value) - relation = table.where(sql, *params) + relation = finder_class.unscoped.where(sql, *params) Array.wrap(options[:scope]).each do |scope_item| scope_value = record.send(scope_item) relation = relation.where(scope_item => scope_value) end - unless record.new_record? + if record.persisted? # TODO : This should be in Arel relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id)) end @@ -154,33 +151,25 @@ module ActiveRecord # | # title! # # This could even happen if you use transactions with the 'serializable' - # isolation level. There are several ways to get around this problem: - # - # - By locking the database table before validating, and unlocking it after - # saving. However, table locking is very expensive, and thus not - # recommended. - # - By locking a lock file before validating, and unlocking it after saving. - # This does not work if you've scaled your Rails application across - # multiple web servers (because they cannot share lock files, or cannot - # do that efficiently), and thus not recommended. - # - Creating a unique index on the field, by using - # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the - # rare case that a race condition occurs, the database will guarantee - # the field's uniqueness. + # isolation level. The best way to work around this problem is to add a unique + # index to the database table using + # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the + # rare case that a race condition occurs, the database will guarantee + # the field's uniqueness. # - # When the database catches such a duplicate insertion, - # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid - # exception. You can either choose to let this error propagate (which - # will result in the default Rails exception page being shown), or you - # can catch it and restart the transaction (e.g. by telling the user - # that the title already exists, and asking him to re-enter the title). - # This technique is also known as optimistic concurrency control: - # http://en.wikipedia.org/wiki/Optimistic_concurrency_control + # When the database catches such a duplicate insertion, + # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid + # exception. You can either choose to let this error propagate (which + # will result in the default Rails exception page being shown), or you + # can catch it and restart the transaction (e.g. by telling the user + # that the title already exists, and asking him to re-enter the title). + # This technique is also known as optimistic concurrency control: + # http://en.wikipedia.org/wiki/Optimistic_concurrency_control # - # Active Record currently provides no way to distinguish unique - # index constraint errors from other types of database errors, so you - # will have to parse the (database-specific) exception message to detect - # such a case. + # Active Record currently provides no way to distinguish unique + # index constraint errors from other types of database errors, so you + # will have to parse the (database-specific) exception message to detect + # such a case. # def validates_uniqueness_of(*attr_names) validates_with UniquenessValidator, _merge_attributes(attr_names) diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 89eba15be1..0667be7d23 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -3,8 +3,8 @@ module ActiveRecord MAJOR = 3 MINOR = 1 TINY = 0 - BUILD = "beta" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') 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 8ac21c1410..126b6f434b 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -1,5 +1,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration - def self.up + def up <% attributes.each do |attribute| -%> <%- if migration_action -%> <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end %> @@ -7,7 +7,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration <%- end -%> end - def self.down + def 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 %> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb index 1f68487304..70e064be21 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb @@ -1,5 +1,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration - def self.up + def up create_table :<%= table_name %> do |t| <% for attribute in attributes -%> t.<%= attribute.type %> :<%= attribute.name %> @@ -10,7 +10,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration end end - def self.down + def down drop_table :<%= table_name %> end end diff --git a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb index 919822af7b..8f0bf1ef0d 100644 --- a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb @@ -1,5 +1,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration - def self.up + def up create_table :<%= session_table_name %> do |t| t.string :session_id, :null => false t.text :data @@ -10,7 +10,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration add_index :<%= session_table_name %>, :updated_at end - def self.down + def down drop_table :<%= session_table_name %> end end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index f76a23a8ad..62ffde558f 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -43,6 +43,64 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert @connection.active? end + def test_bind_value_substitute + bind_param = @connection.substitute_for('foo', []) + assert_equal Arel.sql('?'), bind_param + end + + def test_exec_no_binds + @connection.exec_query('drop table if exists ex') + @connection.exec_query(<<-eosql) + CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + `data` varchar(255)) + eosql + result = @connection.exec_query('SELECT id, data FROM ex') + assert_equal 0, result.rows.length + assert_equal 2, result.columns.length + assert_equal %w{ id data }, result.columns + + @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @connection.exec_query('SELECT id, data FROM ex') + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end + + def test_exec_with_binds + @connection.exec_query('drop table if exists ex') + @connection.exec_query(<<-eosql) + CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + `data` varchar(255)) + eosql + @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @connection.exec_query( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end + + def test_exec_typecasts_bind_vals + @connection.exec_query('drop table if exists ex') + @connection.exec_query(<<-eosql) + CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + `data` varchar(255)) + eosql + @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + column = @connection.columns('ex').find { |col| col.name == 'id' } + + result = @connection.exec_query( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end + # Test that MySQL allows multiple results for stored procedures if Mysql.const_defined?(:CLIENT_MULTI_RESULTS) def test_multi_results diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 7b72151b57..b0a4a4e39d 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -5,6 +5,8 @@ module ActiveRecord class PostgreSQLAdapterTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection + @connection.exec_query('drop table if exists ex') + @connection.exec_query('create table ex(id serial primary key, data character varying(255))') end def test_table_alias_length @@ -12,6 +14,55 @@ module ActiveRecord @connection.table_alias_length end end + + def test_exec_no_binds + result = @connection.exec_query('SELECT id, data FROM ex') + assert_equal 0, result.rows.length + assert_equal 2, result.columns.length + assert_equal %w{ id data }, result.columns + + string = @connection.quote('foo') + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + result = @connection.exec_query('SELECT id, data FROM ex') + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [['1', 'foo']], result.rows + end + + def test_exec_with_binds + string = @connection.quote('foo') + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + result = @connection.exec_query( + 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [['1', 'foo']], result.rows + end + + def test_exec_typecasts_bind_vals + string = @connection.quote('foo') + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + + column = @connection.columns('ex').find { |col| col.name == 'id' } + result = @connection.exec_query( + 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [['1', 'foo']], result.rows + end + + def test_substitute_for + bind = @connection.substitute_for(nil, []) + assert_equal Arel.sql('$1'), bind + + bind = @connection.substitute_for(nil, [nil]) + assert_equal Arel.sql('$2'), bind + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 6f372edc38..d5e1838543 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -43,6 +43,36 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase end end + def test_session_auth= + assert_raise(ActiveRecord::StatementInvalid) do + @connection.session_auth = 'DEFAULT' + @connection.execute "SELECT * FROM #{TABLE_NAME}" + end + end + + def test_setting_auth_clears_stmt_cache + assert_nothing_raised do + set_session_auth + USERS.each do |u| + set_session_auth u + assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name'] + set_session_auth + end + end + end + + def test_auth_with_bind + assert_nothing_raised do + set_session_auth + USERS.each do |u| + @connection.clear_cache! + set_session_auth u + assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name'] + set_session_auth + end + end + end + def test_schema_uniqueness assert_nothing_raised do set_session_auth @@ -78,7 +108,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase private def set_session_auth auth = nil - @connection.execute "SET SESSION AUTHORIZATION #{auth || 'default'}" + @connection.session_auth = auth || 'default' end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 934cf72f72..234696adeb 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -3,6 +3,12 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class SQLite3AdapterTest < ActiveRecord::TestCase + def setup + @conn = Base.sqlite3_connection :database => ':memory:', + :adapter => 'sqlite3', + :timeout => 100 + end + def test_connection_no_db assert_raises(ArgumentError) do Base.sqlite3_connection {} @@ -38,18 +44,58 @@ module ActiveRecord end def test_connect - conn = Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => 100 - assert conn, 'should have connection' + 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 + assert_equal 'UTF-8', @conn.encoding + end + + def test_bind_value_substitute + bind_param = @conn.substitute_for('foo', []) + assert_equal Arel.sql('?'), bind_param + end + + def test_exec_no_binds + @conn.exec_query('create table ex(id int, data string)') + result = @conn.exec_query('SELECT id, data FROM ex') + assert_equal 0, result.rows.length + assert_equal 2, result.columns.length + assert_equal %w{ id data }, result.columns + + @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @conn.exec_query('SELECT id, data FROM ex') + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end + + def test_exec_query_with_binds + @conn.exec_query('create table ex(id int, data string)') + @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @conn.exec_query( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end + + def test_exec_query_typecasts_bind_vals + @conn.exec_query('create table ex(id int, data string)') + @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + column = @conn.columns('ex').find { |col| col.name == 'id' } + + result = @conn.exec_query( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index cbaa4990f7..1b0c00bd5a 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -81,6 +81,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key") end + def test_eager_loading_with_primary_key_as_symbol + Firm.create("name" => "Apple") + Client.create("name" => "Citibank", :firm_name => "Apple") + citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key_symbols) + assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key_symbols") + end + def test_no_unexpected_aliasing first_firm = companies(:first_firm) another_firm = companies(:another_firm) @@ -278,10 +285,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase final_cut = Client.new("name" => "Final Cut") firm = Firm.find(1) final_cut.firm = firm - assert final_cut.new_record? + assert !final_cut.persisted? assert final_cut.save - assert !final_cut.new_record? - assert !firm.new_record? + assert final_cut.persisted? + assert firm.persisted? assert_equal firm, final_cut.firm assert_equal firm, final_cut.firm(true) end @@ -290,10 +297,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase final_cut = Client.new("name" => "Final Cut") firm = Firm.find(1) final_cut.firm_with_primary_key = firm - assert final_cut.new_record? + assert !final_cut.persisted? assert final_cut.save - assert !final_cut.new_record? - assert !firm.new_record? + assert final_cut.persisted? + assert firm.persisted? assert_equal firm, final_cut.firm_with_primary_key assert_equal firm, final_cut.firm_with_primary_key(true) end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index c7c32da9f3..0742e311d9 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -3,13 +3,15 @@ require 'models/post' require 'models/comment' require 'models/author' require 'models/categorization' +require 'models/category' require 'models/company' require 'models/topic' require 'models/reply' require 'models/person' class CascadedEagerLoadingTest < ActiveRecord::TestCase - fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations, :people + fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, + :categorizations, :people, :categories def test_eager_association_loading_with_cascaded_two_levels authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") @@ -45,6 +47,31 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase 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_cascaded_eager_association_loading_with_join_for_count + categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors]) + + assert_nothing_raised do + assert_equal 2, categories.count + assert_equal 2, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes + end + end + + def test_cascaded_eager_association_loading_with_duplicated_includes + categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null") + assert_nothing_raised do + assert_equal 2, categories.count + assert_equal 2, categories.all.size + end + end + + def test_cascaded_eager_association_loading_with_twice_includes_edge_cases + categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null") + assert_nothing_raised do + assert_equal 2, categories.count + assert_equal 2, categories.all.size + end + end + def test_eager_association_loading_with_join_for_count authors = Author.joins(:special_posts).includes([:posts, :categorizations]) @@ -111,7 +138,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_multiple_stis_and_order - author = Author.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => 'authors.name, comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4') + author = Author.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => ['authors.name', 'comments.body', 'very_special_comments_posts.body'], :conditions => 'posts.id = 4') assert_equal authors(:david), author assert_no_queries do author.posts.first.special_comments diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index b124a2bfc3..fb59f63f91 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -17,7 +17,7 @@ class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase def generate_test_objects post = Namespaced::Post.create( :title => 'Great stuff', :body => 'This is not', :author_id => 1 ) - tagging = Tagging.create( :taggable => post ) + Tagging.create( :taggable => post ) end def test_class_names diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 2d8e02e398..ea86ac29d0 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -79,6 +79,58 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle + Post.connection.expects(:in_clause_length).at_least_once.returns(5) + posts = Post.find(:all, :include=>:comments) + assert_equal 7, posts.size + end + + def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle + Post.connection.expects(:in_clause_length).at_least_once.returns(nil) + posts = Post.find(:all, :include=>:comments) + assert_equal 7, posts.size + end + + def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle + Post.connection.expects(:in_clause_length).at_least_once.returns(5) + posts = Post.find(:all, :include=>:categories) + assert_equal 7, posts.size + end + + def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle + Post.connection.expects(:in_clause_length).at_least_once.returns(nil) + posts = Post.find(:all, :include=>:categories) + assert_equal 7, posts.size + end + + def test_load_associated_records_in_one_query_when_adapter_has_no_limit + Post.connection.expects(:in_clause_length).at_least_once.returns(nil) + Post.expects(:i_was_called).with([1,2,3,4,5,6,7]).returns([1]) + associated_records = Post.send(:associated_records, [1,2,3,4,5,6,7]) do |some_ids| + Post.i_was_called(some_ids) + end + assert_equal [1], associated_records + end + + def test_load_associated_records_in_several_queries_when_many_ids_passed + Post.connection.expects(:in_clause_length).at_least_once.returns(5) + Post.expects(:i_was_called).with([1,2,3,4,5]).returns([1]) + Post.expects(:i_was_called).with([6,7]).returns([6]) + associated_records = Post.send(:associated_records, [1,2,3,4,5,6,7]) do |some_ids| + Post.i_was_called(some_ids) + end + assert_equal [1,6], associated_records + end + + def test_load_associated_records_in_one_query_when_a_few_ids_passed + Post.connection.expects(:in_clause_length).at_least_once.returns(5) + Post.expects(:i_was_called).with([1,2,3]).returns([1]) + associated_records = Post.send(:associated_records, [1,2,3]) do |some_ids| + Post.i_was_called(some_ids) + end + assert_equal [1], associated_records + end + def test_including_duplicate_objects_from_belongs_to popular_post = Post.create!(:title => 'foo', :body => "I like cars!") comment = popular_post.comments.create!(:body => "lol") @@ -323,7 +375,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_through_a_belongs_to_association author = authors(:mary) - post = Post.create!(:author => author, :title => "TITLE", :body => "BODY") + Post.create!(:author => author, :title => "TITLE", :body => "BODY") author.author_favorites.create(:favorite_author_id => 1) author.author_favorites.create(:favorite_author_id => 2) posts_with_author_favorites = author.posts.find(:all, :include => :author_favorites) @@ -521,7 +573,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_inheritance - posts = SpecialPost.find(:all, :include => [ :comments ]) + SpecialPost.find(:all, :include => [ :comments ]) end def test_eager_has_one_with_association_inheritance @@ -561,16 +613,16 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_invalid_association_reference assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - post = Post.find(6, :include=> :monkeys ) + Post.find(6, :include=> :monkeys ) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - post = Post.find(6, :include=>[ :monkeys ]) + Post.find(6, :include=>[ :monkeys ]) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - post = Post.find(6, :include=>[ 'monkeys' ]) + Post.find(6, :include=>[ 'monkeys' ]) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { - post = Post.find(6, :include=>[ :monkeys, :elephants ]) + Post.find(6, :include=>[ :monkeys, :elephants ]) } end @@ -584,8 +636,8 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_limited_eager_with_multiple_order_columns - assert_equal posts(:thinking, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title), posts.id', :limit => 2, :offset => 1) - assert_equal posts(:sti_post_and_comments, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC, posts.id', :limit => 2, :offset => 1) + assert_equal posts(:thinking, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => ['UPPER(posts.title)', 'posts.id'], :limit => 2, :offset => 1) + assert_equal posts(:sti_post_and_comments, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => ['UPPER(posts.title) DESC', 'posts.id'], :limit => 2, :offset => 1) end def test_limited_eager_with_numeric_in_association diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 7e070e1746..705550216c 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -251,10 +251,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase no_of_projects = Project.count aredridel = Developer.new("name" => "Aredridel") aredridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")]) - assert aredridel.new_record? - assert p.new_record? + assert !aredridel.persisted? + assert !p.persisted? assert aredridel.save - assert !aredridel.new_record? + assert aredridel.persisted? assert_equal no_of_devels+1, Developer.count assert_equal no_of_projects+1, Project.count assert_equal 2, aredridel.projects.size @@ -288,22 +288,22 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal devel.projects.last, proj assert devel.projects.loaded? - assert proj.new_record? + assert !proj.persisted? devel.save - assert !proj.new_record? + assert proj.persisted? assert_equal devel.projects.last, proj assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end def test_build_by_new_record devel = Developer.new(:name => "Marcel", :salary => 75000) - proj1 = devel.projects.build(:name => "Make bed") + devel.projects.build(:name => "Make bed") proj2 = devel.projects.build(:name => "Lie in it") assert_equal devel.projects.last, proj2 - assert proj2.new_record? + assert !proj2.persisted? devel.save - assert !devel.new_record? - assert !proj2.new_record? + assert devel.persisted? + assert proj2.persisted? assert_equal devel.projects.last, proj2 assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated end @@ -316,19 +316,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal devel.projects.last, proj assert !devel.projects.loaded? - assert !proj.new_record? + assert proj.persisted? assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end def test_create_by_new_record devel = Developer.new(:name => "Marcel", :salary => 75000) - proj1 = devel.projects.build(:name => "Make bed") + devel.projects.build(:name => "Make bed") proj2 = devel.projects.build(:name => "Lie in it") assert_equal devel.projects.last, proj2 - assert proj2.new_record? + assert !proj2.persisted? devel.save - assert !devel.new_record? - assert !proj2.new_record? + assert devel.persisted? + assert proj2.persisted? assert_equal devel.projects.last, proj2 assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated end @@ -343,7 +343,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase # in Oracle '' is saved as null therefore need to save ' ' in not null column another_post = categories(:general).post_with_conditions.create(:body => ' ') - assert !another_post.new_record? + assert another_post.persisted? assert_equal 'Yet Another Testing Title', another_post.title end @@ -559,8 +559,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_dynamic_find_should_respect_association_order # Developers are ordered 'name DESC, id DESC' - low_id_jamis = developers(:jamis) - middle_id_jamis = developers(:poor_jamis) high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis') assert_equal high_id_jamis, projects(:active_record).developers.find(:first, :conditions => "name = 'Jamis'") diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index c9f00fd737..fb772bb8e6 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -66,6 +66,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 'exotic', bulb.name end + def test_no_sql_should_be_fired_if_association_already_loaded + Car.create(:name => 'honda') + bulbs = Car.first.bulbs + bulbs.inspect # to load all instances of bulbs + assert_no_queries do + bulbs.first() + bulbs.first({}) + end + end + def test_create_resets_cached_counters person = Person.create!(:first_name => 'tenderlove') post = Post.first @@ -73,7 +83,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [], person.readers assert_nil person.readers.find_by_post_id(post.id) - reader = person.readers.create(:post_id => post.id) + person.readers.create(:post_id => post.id) assert_equal 1, person.readers.count assert_equal 1, person.readers.length @@ -88,7 +98,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase 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) + person.readers.find_or_create_by_post_id(post.id) assert_equal 1, person.readers.count assert_equal 1, person.readers.length @@ -187,7 +197,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase another = author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") assert_equal number_of_posts + 1, Post.count assert_equal another, author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") - assert !another.new_record? + assert another.persisted? end def test_cant_save_has_many_readonly_association @@ -402,7 +412,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_adding_using_create first_firm = companies(:first_firm) assert_equal 2, first_firm.plain_clients.size - natural = first_firm.plain_clients.create(:name => "Natural Company") + first_firm.plain_clients.create(:name => "Natural Company") assert_equal 3, first_firm.plain_clients.length assert_equal 3, first_firm.plain_clients.size end @@ -453,7 +463,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !company.clients_of_firm.loaded? assert_equal "Another Client", new_client.name - assert new_client.new_record? + assert !new_client.persisted? assert_equal new_client, company.clients_of_firm.last end @@ -483,7 +493,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_build_followed_by_save_does_not_load_target - new_client = companies(:first_firm).clients_of_firm.build("name" => "Another Client") + companies(:first_firm).clients_of_firm.build("name" => "Another Client") assert companies(:first_firm).save assert !companies(:first_firm).clients_of_firm.loaded? end @@ -508,7 +518,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !company.clients_of_firm.loaded? assert_equal "Another Client", new_client.name - assert new_client.new_record? + assert !new_client.persisted? assert_equal new_client, company.clients_of_firm.last end @@ -543,7 +553,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create force_signal37_to_load_all_clients_of_firm new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client") - assert !new_client.new_record? + assert new_client.persisted? assert_equal new_client, companies(:first_firm).clients_of_firm.last assert_equal new_client, companies(:first_firm).clients_of_firm(true).last end @@ -554,7 +564,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_create_followed_by_save_does_not_load_target - new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client") + companies(:first_firm).clients_of_firm.create("name" => "Another Client") assert companies(:first_firm).save assert !companies(:first_firm).clients_of_firm.loaded? end @@ -563,7 +573,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase the_client = companies(:first_firm).clients.find_or_initialize_by_name("Yet another client") assert_equal companies(:first_firm).id, the_client.firm_id assert_equal "Yet another client", the_client.name - assert the_client.new_record? + assert !the_client.persisted? end def test_find_or_create_updates_size @@ -752,7 +762,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create - assert !another_ms_client.new_record? + assert another_ms_client.persisted? assert_equal 'Microsoft', another_ms_client.name end @@ -852,7 +862,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_dependence_for_associations_with_hash_condition david = authors(:david) - post = posts(:thinking).id assert_difference('Post.count', -1) { assert david.destroy } end @@ -872,7 +881,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_three_levels_of_dependence topic = Topic.create "title" => "neat and simple" reply = topic.replies.create "title" => "neat and simple", "content" => "still digging it" - silly_reply = reply.replies.create "title" => "neat and simple", "content" => "ain't complaining" + reply.replies.create "title" => "neat and simple", "content" => "ain't complaining" assert_nothing_raised { topic.destroy } end @@ -897,7 +906,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_depends_and_nullify num_accounts = Account.count - num_companies = Company.count core = companies(:rails_core) assert_equal accounts(:rails_core_account), core.account @@ -914,7 +922,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_restrict firm = RestrictedFirm.new(:name => 'restrict') firm.save! - child_firm = firm.companies.create(:name => 'child') + firm.companies.create(:name => 'child') assert !firm.companies.empty? assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } end @@ -1273,4 +1281,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase comment = post.comments.build assert post.comments.include?(comment) end + + def test_load_target_respects_protected_attributes + topic = Topic.create! + reply = topic.replies.create(:title => "reply 1") + reply.approved = false + reply.save! + + # Save with a different object instance, so the instance that's still held + # in topic.relies doesn't know about the changed attribute. + reply2 = Reply.find(reply.id) + reply2.approved = true + reply2.save! + + # Force loading the collection from the db. This will merge the existing + # object (reply) with what gets loaded from the db (which includes the + # changed approved attribute). approved is a protected attribute, so if mass + # assignment is used, it won't get updated and will still be false. + first = topic.replies.to_a.first + assert_equal reply.id, first.id + assert_equal true, first.approved? + end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 4b9f49f1ec..52432b0428 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -354,7 +354,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_association_through_a_belongs_to_association_where_the_association_doesnt_exist - author = authors(:mary) post = Post.create!(:title => "TITLE", :body => "BODY") assert_equal [], post.author_favorites end @@ -449,4 +448,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase comment = post.comments.build assert author.comments.include?(comment) end + + def test_size_of_through_association_should_increase_correctly_when_has_many_association_is_added + post = posts(:thinking) + readers = post.readers.size + post.people << people(:michael) + assert_equal readers + 1, post.readers.size + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index b522be3fe0..64449df8f5 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -163,7 +163,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase firm = ExclusivelyDependentFirm.find(9) assert_not_nil firm.account - account_id = firm.account.id assert_equal [], Account.destroyed_account_ids[firm.id] firm.destroy @@ -180,7 +179,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_dependence_with_restrict firm = RestrictedFirm.new(:name => 'restrict') firm.save! - account = firm.create_account(:credit_limit => 10) + firm.create_account(:credit_limit => 10) assert_not_nil firm.account assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } end @@ -197,8 +196,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_build_association_twice_without_saving_affects_nothing count_of_account = Account.count firm = Firm.find(:first) - account1 = firm.build_account("credit_limit" => 1000) - account2 = firm.build_account("credit_limit" => 2000) + firm.build_account("credit_limit" => 1000) + firm.build_account("credit_limit" => 2000) assert_equal count_of_account, Account.count end @@ -268,7 +267,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_assignment_before_child_saved firm = Firm.find(1) firm.account = a = Account.new("credit_limit" => 1000) - assert !a.new_record? + assert a.persisted? assert_equal a, firm.account assert_equal a, firm.account assert_equal a, firm.account(true) @@ -323,7 +322,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_create_respects_hash_condition account = companies(:first_firm).create_account_limit_500_with_hash_conditions - assert !account.new_record? + assert account.persisted? assert_equal 500, account.credit_limit end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index fa5c2e49df..0491b2d1fa 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -484,7 +484,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_child_instance_should_be_shared_with_replaced_via_accessor_parent face = faces(:confused) - old_man = face.polymorphic_man new_man = Man.new assert_not_nil face.polymorphic_man @@ -499,7 +498,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_child_instance_should_be_shared_with_replaced_via_method_parent face = faces(:confused) - old_man = face.polymorphic_man new_man = Man.new assert_not_nil face.polymorphic_man @@ -551,8 +549,8 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do i = Interest.find(:first) - z = i.zine - m = i.man + i.zine + i.man end end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 7a22ce4dad..1ece961d2e 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -450,11 +450,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase new_tag = Tag.new(:name => "new") saved_post.tags << new_tag - assert !new_tag.new_record? #consistent with habtm! - assert !saved_post.new_record? + assert new_tag.persisted? #consistent with habtm! + assert saved_post.persisted? assert saved_post.tags.include?(new_tag) - assert !new_tag.new_record? + assert new_tag.persisted? assert saved_post.reload.tags(true).include?(new_tag) @@ -462,16 +462,16 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase saved_tag = tags(:general) new_post.tags << saved_tag - assert new_post.new_record? - assert !saved_tag.new_record? + assert !new_post.persisted? + assert saved_tag.persisted? assert new_post.tags.include?(saved_tag) new_post.save! - assert !new_post.new_record? + assert new_post.persisted? assert new_post.reload.tags(true).include?(saved_tag) - assert posts(:thinking).tags.build.new_record? - assert posts(:thinking).tags.new.new_record? + assert !posts(:thinking).tags.build.persisted? + assert !posts(:thinking).tags.new.persisted? end def test_create_associate_when_adding_to_has_many_through @@ -642,7 +642,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_preload_nil_polymorphic_belongs_to assert_nothing_raised do - taggings = Tagging.find(:all, :include => :taggable, :conditions => ['taggable_type IS NULL']) + Tagging.find(:all, :include => :taggable, :conditions => ['taggable_type IS NULL']) end end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index dd8152b219..83c605d2bb 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -120,7 +120,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_force_reload_is_uncached firm = Firm.create!("name" => "A New Firm, Inc") - client = Client.create!("name" => "TheClient.com", :firm => firm) + Client.create!("name" => "TheClient.com", :firm => firm) ActiveRecord::Base.cache do firm.clients.each {} assert_queries(0) { assert_not_nil firm.clients.each {} } @@ -270,17 +270,17 @@ class OverridingAssociationsTest < ActiveRecord::TestCase def test_habtm_association_redefinition_callbacks_should_differ_and_not_inherited # redeclared association on AR descendant should not inherit callbacks from superclass - callbacks = PeopleList.read_inheritable_attribute(:before_add_for_has_and_belongs_to_many) + callbacks = PeopleList.before_add_for_has_and_belongs_to_many assert_equal([:enlist], callbacks) - callbacks = DifferentPeopleList.read_inheritable_attribute(:before_add_for_has_and_belongs_to_many) + callbacks = DifferentPeopleList.before_add_for_has_and_belongs_to_many assert_equal([], callbacks) end def test_has_many_association_redefinition_callbacks_should_differ_and_not_inherited # redeclared association on AR descendant should not inherit callbacks from superclass - callbacks = PeopleList.read_inheritable_attribute(:before_add_for_has_many) + callbacks = PeopleList.before_add_for_has_many assert_equal([:enlist], callbacks) - callbacks = DifferentPeopleList.read_inheritable_attribute(:before_add_for_has_many) + callbacks = DifferentPeopleList.before_add_for_has_many assert_equal([], callbacks) end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 1750bf004a..bb0166a60c 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -103,7 +103,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase if current_adapter?(:MysqlAdapter) def test_read_attributes_before_type_cast_on_boolean bool = Boolean.create({ "value" => false }) - assert_equal "0", bool.reload.attributes_before_type_cast["value"] + assert_equal 0, bool.reload.attributes_before_type_cast["value"] end end @@ -112,7 +112,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase developer = Developer.find(:first) # Oracle adapter returns Time before type cast unless current_adapter?(:OracleAdapter) - assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"] + assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"].to_s else assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"].to_s(:db) @@ -121,7 +121,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal developer.created_at, nil developer.created_at = "2010-03-21 21:23:32" - assert_equal developer.created_at_before_type_cast, "2010-03-21 21:23:32" + assert_equal developer.created_at_before_type_cast.to_s, "2010-03-21 21:23:32" assert_equal developer.created_at, Time.parse("2010-03-21 21:23:32") end end @@ -568,7 +568,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_bulk_update_respects_access_control privatize("title=(value)") - assert_raise(ActiveRecord::UnknownAttributeError) { topic = @target.new(:title => "Rants about pants") } + assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(:title => "Rants about pants") } assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 89be94c81f..fbf7121468 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -83,7 +83,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert !firm.build_account_using_primary_key.valid? assert firm.save - assert firm.account_using_primary_key.new_record? + assert !firm.account_using_primary_key.persisted? end def test_save_fails_for_invalid_has_one @@ -114,10 +114,10 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas account = firm.account.build("credit_limit" => 1000) assert_equal account, firm.account - assert account.new_record? + assert !account.persisted? assert firm.save assert_equal account, firm.account - assert !account.new_record? + assert account.persisted? end def test_build_before_either_saved @@ -125,16 +125,16 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas firm.account = account = Account.new("credit_limit" => 1000) assert_equal account, firm.account - assert account.new_record? + assert !account.persisted? assert firm.save assert_equal account, firm.account - assert !account.new_record? + assert account.persisted? end def test_assignment_before_parent_saved firm = Firm.new("name" => "GlobalMegaCorp") firm.account = a = Account.find(1) - assert firm.new_record? + assert !firm.persisted? assert_equal a, firm.account assert firm.save assert_equal a, firm.account @@ -144,12 +144,12 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas def test_assignment_before_either_saved firm = Firm.new("name" => "GlobalMegaCorp") firm.account = a = Account.new("credit_limit" => 1000) - assert firm.new_record? - assert a.new_record? + assert !firm.persisted? + assert !a.persisted? assert_equal a, firm.account assert firm.save - assert !firm.new_record? - assert !a.new_record? + assert firm.persisted? + assert a.persisted? assert_equal a, firm.account assert_equal a, firm.account(true) end @@ -163,15 +163,15 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas firm.account = Account.find(:first) assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! } - firm = Firm.find(:first).clone + firm = Firm.find(:first).dup firm.account = Account.find(:first) assert_queries(2) { firm.save! } - firm = Firm.find(:first).clone - firm.account = Account.find(:first).clone + firm = Firm.find(:first).dup + firm.account = Account.find(:first).dup assert_queries(2) { firm.save! } end - + def test_callbacks_firing_order_on_create eye = Eye.create(:iris_attributes => {:color => 'honey'}) assert_equal [true, false], eye.after_create_callbacks_stack @@ -203,7 +203,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert !client.firm.valid? assert client.save - assert client.firm.new_record? + assert !client.firm.persisted? end def test_save_fails_for_invalid_belongs_to @@ -232,10 +232,10 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test apple = Firm.new("name" => "Apple") client.firm = apple assert_equal apple, client.firm - assert apple.new_record? + assert !apple.persisted? assert client.save assert apple.save - assert !apple.new_record? + assert apple.persisted? assert_equal apple, client.firm assert_equal apple, client.firm(true) end @@ -244,11 +244,11 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test final_cut = Client.new("name" => "Final Cut") apple = Firm.new("name" => "Apple") final_cut.firm = apple - assert final_cut.new_record? - assert apple.new_record? + assert !final_cut.persisted? + assert !apple.persisted? assert final_cut.save - assert !final_cut.new_record? - assert !apple.new_record? + assert final_cut.persisted? + assert apple.persisted? assert_equal apple, final_cut.firm assert_equal apple, final_cut.firm(true) end @@ -348,23 +348,21 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_invalid_adding firm = Firm.find(1) assert !(firm.clients_of_firm << c = Client.new) - assert c.new_record? + assert !c.persisted? assert !firm.valid? assert !firm.save - assert c.new_record? + assert !c.persisted? end def test_invalid_adding_before_save - no_of_firms = Firm.count - no_of_clients = Client.count new_firm = Firm.new("name" => "A New Firm, Inc") new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")]) - assert c.new_record? + assert !c.persisted? assert !c.valid? assert !new_firm.valid? assert !new_firm.save - assert c.new_record? - assert new_firm.new_record? + assert !c.persisted? + assert !new_firm.persisted? end def test_invalid_adding_with_validate_false @@ -375,7 +373,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert firm.valid? assert !client.valid? assert firm.save - assert client.new_record? + assert !client.persisted? end def test_valid_adding_with_validate_false @@ -386,22 +384,22 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert firm.valid? assert client.valid? - assert client.new_record? + assert !client.persisted? firm.unvalidated_clients_of_firm << client assert firm.save - assert !client.new_record? + assert client.persisted? assert_equal no_of_clients + 1, Client.count end def test_invalid_build new_client = companies(:first_firm).clients_of_firm.build - assert new_client.new_record? + assert !new_client.persisted? assert !new_client.valid? assert_equal new_client, companies(:first_firm).clients_of_firm.last assert !companies(:first_firm).save - assert new_client.new_record? + assert !new_client.persisted? assert_equal 1, companies(:first_firm).clients_of_firm(true).size end @@ -420,8 +418,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal no_of_firms, Firm.count # Firm was not saved to database. assert_equal no_of_clients, Client.count # Clients were not saved to database. assert new_firm.save - assert !new_firm.new_record? - assert !c.new_record? + assert new_firm.persisted? + assert c.persisted? assert_equal new_firm, c.firm assert_equal no_of_firms + 1, Firm.count # Firm was saved to database. assert_equal no_of_clients + 2, Client.count # Clients were saved to database. @@ -455,13 +453,13 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } - assert !new_client.new_record? + assert new_client.persisted? assert_equal 2, company.clients_of_firm(true).size end def test_build_many_before_save company = companies(:first_firm) - new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } + assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } company.name += '-changed' assert_queries(3) { assert company.save } @@ -475,13 +473,13 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } - assert !new_client.new_record? + assert new_client.persisted? assert_equal 2, company.clients_of_firm(true).size end def test_build_many_via_block_before_save company = companies(:first_firm) - new_clients = assert_no_queries do + assert_no_queries do company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client| client.name = "changed" end @@ -507,62 +505,62 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase new_account = Account.new("credit_limit" => 1000) new_firm = Firm.new("name" => "some firm") - assert new_firm.new_record? + assert !new_firm.persisted? new_account.firm = new_firm new_account.save! - assert !new_firm.new_record? + assert new_firm.persisted? new_account = Account.new("credit_limit" => 1000) new_autosaved_firm = Firm.new("name" => "some firm") - assert new_autosaved_firm.new_record? + assert !new_autosaved_firm.persisted? new_account.unautosaved_firm = new_autosaved_firm new_account.save! - assert new_autosaved_firm.new_record? + assert !new_autosaved_firm.persisted? end def test_autosave_new_record_on_has_one_can_be_disabled_per_relationship firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) - assert account.new_record? + assert !account.persisted? firm.account = account firm.save! - assert !account.new_record? + assert account.persisted? firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) firm.unautosaved_account = account - assert account.new_record? + assert !account.persisted? firm.unautosaved_account = account firm.save! - assert account.new_record? + assert !account.persisted? end def test_autosave_new_record_on_has_many_can_be_disabled_per_relationship firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) - assert account.new_record? + assert !account.persisted? firm.accounts << account firm.save! - assert !account.new_record? + assert account.persisted? firm = Firm.new("name" => "some firm") account = Account.new("credit_limit" => 1000) - assert account.new_record? + assert !account.persisted? firm.unautosaved_accounts << account firm.save! - assert account.new_record? + assert !account.persisted? end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index e63e1fbe09..c3ba1f0c35 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -182,7 +182,7 @@ class BasicsTest < ActiveRecord::TestCase def test_initialize_with_invalid_attribute begin - topic = Topic.new({ "title" => "test", + Topic.new({ "title" => "test", "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31"}) rescue ActiveRecord::MultiparameterAssignmentErrors => ex assert_equal(1, ex.errors.size) @@ -397,6 +397,15 @@ class BasicsTest < ActiveRecord::TestCase assert_not_equal Topic.new, Topic.new end + def test_equality_of_destroyed_records + topic_1 = Topic.new(:title => 'test_1') + topic_1.save + topic_2 = Topic.find(topic_1.id) + topic_1.destroy + assert_equal topic_1, topic_2 + assert_equal topic_2, topic_1 + end + def test_hashing assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] end @@ -656,68 +665,69 @@ class BasicsTest < ActiveRecord::TestCase end def test_new_record_returns_boolean - assert_equal true, Topic.new.new_record? - assert_equal false, Topic.find(1).new_record? + assert_equal false, Topic.new.persisted? + assert_equal true, Topic.find(1).persisted? end - def test_clone + def test_dup topic = Topic.find(1) - cloned_topic = nil - assert_nothing_raised { cloned_topic = topic.clone } - assert_equal topic.title, cloned_topic.title - assert cloned_topic.new_record? + duped_topic = nil + assert_nothing_raised { duped_topic = topic.dup } + assert_equal topic.title, duped_topic.title + assert !duped_topic.persisted? - # test if the attributes have been cloned + # test if the attributes have been duped topic.title = "a" - cloned_topic.title = "b" + duped_topic.title = "b" assert_equal "a", topic.title - assert_equal "b", cloned_topic.title + assert_equal "b", duped_topic.title - # test if the attribute values have been cloned + # test if the attribute values have been duped topic.title = {"a" => "b"} - cloned_topic = topic.clone - cloned_topic.title["a"] = "c" + duped_topic = topic.dup + duped_topic.title["a"] = "c" assert_equal "b", topic.title["a"] - # test if attributes set as part of after_initialize are cloned correctly - assert_equal topic.author_email_address, cloned_topic.author_email_address + # test if attributes set as part of after_initialize are duped correctly + assert_equal topic.author_email_address, duped_topic.author_email_address # test if saved clone object differs from original - cloned_topic.save - assert !cloned_topic.new_record? - assert_not_equal cloned_topic.id, topic.id + duped_topic.save + assert duped_topic.persisted? + assert_not_equal duped_topic.id, topic.id - cloned_topic.reload + duped_topic.reload # FIXME: I think this is poor behavior, and will fix it with #5686 - assert_equal({'a' => 'c'}.to_s, cloned_topic.title) + assert_equal({'a' => 'c'}.to_s, duped_topic.title) end - def test_clone_with_aggregate_of_same_name_as_attribute + def test_dup_with_aggregate_of_same_name_as_attribute dev = DeveloperWithAggregate.find(1) assert_kind_of DeveloperSalary, dev.salary - clone = nil - assert_nothing_raised { clone = dev.clone } - assert_kind_of DeveloperSalary, clone.salary - assert_equal dev.salary.amount, clone.salary.amount - assert clone.new_record? + dup = nil + assert_nothing_raised { dup = dev.dup } + assert_kind_of DeveloperSalary, dup.salary + assert_equal dev.salary.amount, dup.salary.amount + assert !dup.persisted? - # test if the attributes have been cloned - original_amount = clone.salary.amount + # test if the attributes have been dupd + original_amount = dup.salary.amount dev.salary.amount = 1 - assert_equal original_amount, clone.salary.amount + assert_equal original_amount, dup.salary.amount - assert clone.save - assert !clone.new_record? - assert_not_equal clone.id, dev.id + assert dup.save + assert dup.persisted? + assert_not_equal dup.id, dev.id end - def test_clone_does_not_clone_associations + def test_dup_does_not_copy_associations author = authors(:david) assert_not_equal [], author.posts + author.send(:clear_association_cache) - author_clone = author.clone - assert_equal [], author_clone.posts + author_dup = author.dup + assert_equal [], author_dup.posts end def test_clone_preserves_subtype @@ -756,22 +766,22 @@ class BasicsTest < ActiveRecord::TestCase assert !cloned_developer.salary_changed? # ... and cloned instance should behave same end - def test_clone_of_saved_object_marks_attributes_as_dirty + def test_dup_of_saved_object_marks_attributes_as_dirty developer = Developer.create! :name => 'Bjorn', :salary => 100000 assert !developer.name_changed? assert !developer.salary_changed? - cloned_developer = developer.clone + cloned_developer = developer.dup assert cloned_developer.name_changed? # both attributes differ from defaults assert cloned_developer.salary_changed? end - def test_clone_of_saved_object_marks_as_dirty_only_changed_attributes + def test_dup_of_saved_object_marks_as_dirty_only_changed_attributes developer = Developer.create! :name => 'Bjorn' assert !developer.name_changed? # both attributes of saved object should be threated as not changed assert !developer.salary_changed? - cloned_developer = developer.clone + cloned_developer = developer.dup assert cloned_developer.name_changed? # ... but on cloned object should be assert !cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be threated as not changed on cloned instance end @@ -963,7 +973,6 @@ class BasicsTest < ActiveRecord::TestCase end def test_nil_serialized_attribute_with_class_constraint - myobj = MyObject.new('value1', 'value2') topic = Topic.new assert_nil topic.content end @@ -1353,7 +1362,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, group: 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, created_at: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect end def test_inspect_new_instance @@ -1435,10 +1444,6 @@ class BasicsTest < ActiveRecord::TestCase ActiveRecord::Base.logger = original_logger end - def test_dup - assert !Minimalistic.new.freeze.dup.frozen? - end - def test_compute_type_success assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author') end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 61fbf01a50..5cb8485b4b 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -54,6 +54,19 @@ class CalculationsTest < ActiveRecord::TestCase c = Account.sum(:credit_limit, :group => :firm_id) [1,6,2].each { |firm_id| assert c.keys.include?(firm_id) } end + + def test_should_group_by_multiple_fields + c = Account.count(:all, :group => ['firm_id', :credit_limit]) + [ [nil, 50], [1, 50], [6, 50], [6, 55], [9, 53], [2, 60] ].each { |firm_and_limit| assert c.keys.include?(firm_and_limit) } + end + + def test_should_group_by_multiple_fields_having_functions + c = Topic.group(:author_name, 'COALESCE(type, title)').count(:all) + assert_equal 1, c[["Carl", "The Third Topic of the day"]] + assert_equal 1, c[["Mary", "Reply"]] + assert_equal 1, c[["David", "The First Topic"]] + assert_equal 1, c[["Carl", "Reply"]] + end def test_should_group_by_summed_field c = Account.sum(:credit_limit, :group => :firm_id) diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb new file mode 100644 index 0000000000..d91646efca --- /dev/null +++ b/activerecord/test/cases/clone_test.rb @@ -0,0 +1,33 @@ +require "cases/helper" +require 'models/topic' + +module ActiveRecord + class CloneTest < ActiveRecord::TestCase + fixtures :topics + + def test_persisted + topic = Topic.first + cloned = topic.clone + assert topic.persisted?, 'topic persisted' + assert cloned.persisted?, 'topic persisted' + assert !cloned.new_record?, 'topic is not new' + end + + def test_stays_frozen + topic = Topic.first + topic.freeze + + cloned = topic.clone + assert cloned.persisted?, 'topic persisted' + assert !cloned.new_record?, 'topic is not new' + assert cloned.frozen?, 'topic should be frozen' + end + + def test_shallow + topic = Topic.first + cloned = topic.clone + topic.author_name = 'Aaron' + assert_equal 'Aaron', cloned.author_name + end + end +end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index bde93d1c85..a6738fb654 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -338,13 +338,13 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.changed? end - def test_cloned_objects_should_not_copy_dirty_flag_from_creator + def test_dup_objects_should_not_copy_dirty_flag_from_creator pirate = Pirate.create!(:catchphrase => "shiver me timbers") - pirate_clone = pirate.clone - pirate_clone.reset_catchphrase! + pirate_dup = pirate.dup + pirate_dup.reset_catchphrase! pirate.catchphrase = "I love Rum" assert pirate.catchphrase_changed? - assert !pirate_clone.catchphrase_changed? + assert !pirate_dup.catchphrase_changed? end def test_reverted_changes_are_not_dirty @@ -395,6 +395,20 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_save_always_should_update_timestamps_when_serialized_attributes_are_present + with_partial_updates(Topic) do + topic = Topic.create!(:content => {:a => "a"}) + topic.save! + + updated_at = topic.updated_at + topic.content[:hello] = 'world' + topic.save! + + assert_not_equal updated_at, topic.updated_at + assert_equal 'world', topic.content[:hello] + end + end + def test_save_should_not_save_serialized_attribute_with_partial_updates_if_not_present with_partial_updates(Topic) do Topic.create!(:author_name => 'Bill', :content => {:a => "a"}) diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb new file mode 100644 index 0000000000..0236f9b0a1 --- /dev/null +++ b/activerecord/test/cases/dup_test.rb @@ -0,0 +1,103 @@ +require "cases/helper" +require 'models/topic' + +module ActiveRecord + class DupTest < ActiveRecord::TestCase + fixtures :topics + + def test_dup + assert !Topic.new.freeze.dup.frozen? + end + + def test_not_readonly + topic = Topic.first + + duped = topic.dup + assert !duped.readonly?, 'should not be readonly' + end + + def test_is_readonly + topic = Topic.first + topic.readonly! + + duped = topic.dup + assert duped.readonly?, 'should be readonly' + end + + def test_dup_not_persisted + topic = Topic.first + duped = topic.dup + + assert !duped.persisted?, 'topic not persisted' + assert duped.new_record?, 'topic is new' + end + + def test_dup_has_no_id + topic = Topic.first + duped = topic.dup + assert_nil duped.id + end + + def test_dup_with_modified_attributes + topic = Topic.first + topic.author_name = 'Aaron' + duped = topic.dup + assert_equal 'Aaron', duped.author_name + end + + def test_dup_with_changes + dbtopic = Topic.first + topic = Topic.new + + topic.attributes = dbtopic.attributes + + #duped has no timestamp values + duped = dbtopic.dup + + #clear topic timestamp values + topic.send(:clear_timestamp_attributes) + + assert_equal topic.changes, duped.changes + end + + def test_dup_topics_are_independent + topic = Topic.first + topic.author_name = 'Aaron' + duped = topic.dup + + duped.author_name = 'meow' + + assert_not_equal topic.changes, duped.changes + end + + def test_dup_attributes_are_independent + topic = Topic.first + duped = topic.dup + + duped.author_name = 'meow' + topic.author_name = 'Aaron' + + assert_equal 'Aaron', topic.author_name + assert_equal 'meow', duped.author_name + end + + def test_dup_timestamps_are_cleared + topic = Topic.first + assert_not_nil topic.updated_at + assert_not_nil topic.created_at + + # temporary change to the topic object + topic.updated_at -= 3.days + + #dup should not preserve the timestamps if present + new_topic = topic.dup + assert_nil new_topic.updated_at + assert_nil new_topic.created_at + + new_topic.save + assert_not_nil new_topic.updated_at + assert_not_nil new_topic.created_at + end + + end +end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index c058196078..31e4981a1d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -10,6 +10,7 @@ require 'models/entrant' require 'models/project' require 'models/developer' require 'models/customer' +require 'models/toy' class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations @@ -726,7 +727,7 @@ class FinderTest < ActiveRecord::TestCase sig38 = Company.find_or_create_by_name("38signals") assert_equal number_of_companies + 1, Company.count assert_equal sig38, Company.find_or_create_by_name("38signals") - assert !sig38.new_record? + assert sig38.persisted? end def test_find_or_create_from_two_attributes @@ -734,7 +735,7 @@ class FinderTest < ActiveRecord::TestCase another = Topic.find_or_create_by_title_and_author_name("Another topic","John") assert_equal number_of_topics + 1, Topic.count assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John") - assert !another.new_record? + assert another.persisted? end def test_find_or_create_from_two_attributes_with_one_being_an_aggregate @@ -742,7 +743,7 @@ class FinderTest < ActiveRecord::TestCase created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth") assert_equal number_of_customers + 1, Customer.count assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123), "Elizabeth") - assert !created_customer.new_record? + assert created_customer.persisted? end def test_find_or_create_from_one_attribute_and_hash @@ -750,7 +751,7 @@ class FinderTest < ActiveRecord::TestCase sig38 = Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) assert_equal number_of_companies + 1, Company.count assert_equal sig38, Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) - assert !sig38.new_record? + assert sig38.persisted? assert_equal "38signals", sig38.name assert_equal 17, sig38.firm_id assert_equal 23, sig38.client_of @@ -761,7 +762,7 @@ class FinderTest < ActiveRecord::TestCase created_customer = Customer.find_or_create_by_balance(Money.new(123)) assert_equal number_of_customers + 1, Customer.count assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123)) - assert !created_customer.new_record? + assert created_customer.persisted? end def test_find_or_create_from_one_aggregate_attribute_and_hash @@ -771,7 +772,7 @@ class FinderTest < ActiveRecord::TestCase created_customer = Customer.find_or_create_by_balance({:balance => balance, :name => name}) assert_equal number_of_customers + 1, Customer.count assert_equal created_customer, Customer.find_or_create_by_balance({:balance => balance, :name => name}) - assert !created_customer.new_record? + assert created_customer.persisted? assert_equal balance, created_customer.balance assert_equal name, created_customer.name end @@ -779,13 +780,13 @@ class FinderTest < ActiveRecord::TestCase def test_find_or_initialize_from_one_attribute sig38 = Company.find_or_initialize_by_name("38signals") assert_equal "38signals", sig38.name - assert sig38.new_record? + assert !sig38.persisted? end def test_find_or_initialize_from_one_aggregate_attribute new_customer = Customer.find_or_initialize_by_balance(Money.new(123)) assert_equal 123, new_customer.balance.amount - assert new_customer.new_record? + assert !new_customer.persisted? end def test_find_or_initialize_from_one_attribute_should_not_set_attribute_even_when_protected @@ -793,7 +794,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal "Fortune 1000", c.name assert_not_equal 1000, c.rating assert c.valid? - assert c.new_record? + assert !c.persisted? end def test_find_or_create_from_one_attribute_should_not_set_attribute_even_when_protected @@ -801,7 +802,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal "Fortune 1000", c.name assert_not_equal 1000, c.rating assert c.valid? - assert !c.new_record? + assert c.persisted? end def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected @@ -809,7 +810,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal "Fortune 1000", c.name assert_equal 1000, c.rating assert c.valid? - assert c.new_record? + assert !c.persisted? end def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected @@ -817,7 +818,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal "Fortune 1000", c.name assert_equal 1000, c.rating assert c.valid? - assert !c.new_record? + assert c.persisted? end def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash @@ -825,7 +826,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal "Fortune 1000", c.name assert_equal 1000, c.rating assert c.valid? - assert c.new_record? + assert !c.persisted? end def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash @@ -833,7 +834,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal "Fortune 1000", c.name assert_equal 1000, c.rating assert c.valid? - assert !c.new_record? + assert c.persisted? end def test_find_or_initialize_should_set_protected_attributes_if_given_as_block @@ -841,7 +842,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal "Fortune 1000", c.name assert_equal 1000.to_f, c.rating.to_f assert c.valid? - assert c.new_record? + assert !c.persisted? end def test_find_or_create_should_set_protected_attributes_if_given_as_block @@ -849,7 +850,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal "Fortune 1000", c.name assert_equal 1000.to_f, c.rating.to_f assert c.valid? - assert !c.new_record? + assert c.persisted? end def test_find_or_create_should_work_with_block_on_first_call @@ -860,21 +861,21 @@ class FinderTest < ActiveRecord::TestCase assert_equal "Fortune 1000", c.name assert_equal 1000.to_f, c.rating.to_f assert c.valid? - assert !c.new_record? + assert c.persisted? end def test_find_or_initialize_from_two_attributes another = Topic.find_or_initialize_by_title_and_author_name("Another topic","John") assert_equal "Another topic", another.title assert_equal "John", another.author_name - assert another.new_record? + assert !another.persisted? end def test_find_or_initialize_from_one_aggregate_attribute_and_one_not new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth") assert_equal 123, new_customer.balance.amount assert_equal "Elizabeth", new_customer.name - assert new_customer.new_record? + assert !new_customer.persisted? end def test_find_or_initialize_from_one_attribute_and_hash @@ -882,7 +883,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal "38signals", sig38.name assert_equal 17, sig38.firm_id assert_equal 23, sig38.client_of - assert sig38.new_record? + assert !sig38.persisted? end def test_find_or_initialize_from_one_aggregate_attribute_and_hash @@ -891,7 +892,7 @@ class FinderTest < ActiveRecord::TestCase new_customer = Customer.find_or_initialize_by_balance({:balance => balance, :name => name}) assert_equal balance, new_customer.balance assert_equal name, new_customer.name - assert new_customer.new_record? + assert !new_customer.persisted? end def test_find_with_bad_sql @@ -948,7 +949,7 @@ class FinderTest < ActiveRecord::TestCase # http://dev.rubyonrails.org/ticket/6778 def test_find_ignores_previously_inserted_record - post = Post.create!(:title => 'test', :body => 'it out') + Post.create!(:title => 'test', :body => 'it out') assert_equal [], Post.find_all_by_id(nil) end @@ -1010,6 +1011,15 @@ class FinderTest < ActiveRecord::TestCase end end + def test_find_one_message_with_custom_primary_key + Toy.set_primary_key :name + begin + Toy.find 'Hello World!' + rescue ActiveRecord::RecordNotFound => e + assert_equal 'Couldn\'t find Toy with name=Hello World!', e.message + end + end + protected def bind(statement, *vars) if vars.first.is_a?(Hash) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index d5ef30e137..9ce163a00f 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -58,7 +58,7 @@ class FixturesTest < ActiveRecord::TestCase end def test_inserts - topics = create_fixtures("topics") + create_fixtures("topics") first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'David'") assert_equal("The First Topic", first_row["title"]) @@ -114,7 +114,7 @@ class FixturesTest < ActiveRecord::TestCase end def test_insert_with_datetime - topics = create_fixtures("tasks") + create_fixtures("tasks") first = Task.find(1) assert first end @@ -240,7 +240,7 @@ if Account.connection.respond_to?(:reset_pk_sequence!) def test_create_fixtures_resets_sequences_when_not_cached @instances.each do |instance| - max_id = create_fixtures(instance.class.table_name).inject(0) do |_max_id, (name, fixture)| + max_id = create_fixtures(instance.class.table_name).inject(0) do |_max_id, (_, fixture)| fixture_id = fixture['id'].to_i fixture_id > _max_id ? fixture_id : _max_id end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 2d3047c875..f9bbc5299b 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -13,11 +13,6 @@ require 'active_record' require 'active_support/dependencies' require 'connection' -begin - require 'ruby-debug' -rescue LoadError -end - # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true @@ -48,6 +43,10 @@ end ActiveRecord::Base.connection.class.class_eval do IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /SHOW FIELDS/] + # FIXME: this needs to be refactored so specific database can add their own + # ignored SQL. This ignored SQL is for Oracle. + IGNORED_SQL.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from ((all|user)_tab_columns|(all|user)_triggers|(all|user)_constraints)/im] + def execute_with_query_record(sql, name = nil, &block) $queries_executed ||= [] $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r } @@ -55,6 +54,14 @@ ActiveRecord::Base.connection.class.class_eval do end alias_method_chain :execute, :query_record + + def exec_query_with_query_record(sql, name = nil, binds = [], &block) + $queries_executed ||= [] + $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r } + exec_query_without_query_record(sql, name, binds, &block) + end + + alias_method_chain :exec_query, :query_record end ActiveRecord::Base.connection.class.class_eval { diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 31679b2efe..c3da9cdf53 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -16,7 +16,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_class_with_blank_sti_name company = Company.find(:first) - company = company.clone + company = company.dup company.extend(Module.new { def read_attribute(name) return ' ' if name == 'type' diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb new file mode 100644 index 0000000000..afec64750e --- /dev/null +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -0,0 +1,57 @@ +require "cases/helper" + +module ActiveRecord + class InvertibleMigrationTest < ActiveRecord::TestCase + class SilentMigration < ActiveRecord::Migration + def write(text = '') + # sssshhhhh!! + end + end + + class InvertibleMigration < SilentMigration + def change + create_table("horses") do |t| + t.column :content, :text + t.column :remind_at, :datetime + end + end + end + + class NonInvertibleMigration < SilentMigration + def change + create_table("horses") do |t| + t.column :content, :text + t.column :remind_at, :datetime + end + remove_column "horses", :content + end + end + + def teardown + if ActiveRecord::Base.connection.table_exists?("horses") + ActiveRecord::Base.connection.drop_table("horses") + end + end + + def test_no_reverse + migration = NonInvertibleMigration.new + migration.migrate(:up) + assert_raises(IrreversibleMigration) do + migration.migrate(:down) + end + end + + def test_up + migration = InvertibleMigration.new + migration.migrate(:up) + assert migration.connection.table_exists?("horses"), "horses should exist" + end + + def test_down + migration = InvertibleMigration.new + migration.migrate :up + migration.migrate :down + assert !migration.connection.table_exists?("horses") + end + end +end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 4ddcdc010b..f9678cb0c5 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -1,3 +1,4 @@ +require 'thread' require "cases/helper" require 'models/person' require 'models/reader' diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index f3d3d62830..0ffd0e2ab3 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -226,6 +226,12 @@ class MethodScopingTest < ActiveRecord::TestCase assert Post.find(1).comments.include?(new_comment) end + def test_scoped_create_with_join_and_merge + (Comment.where(:body => "but Who's Buying?").joins(:post) & Post.where(:body => 'Peace Sells...')).with_scope do + assert_equal({:body => "but Who's Buying?"}, Comment.scoped.scope_for_create) + end + end + def test_immutable_scope options = { :conditions => "name = 'David'" } Developer.send(:with_scope, :find => options) do diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb new file mode 100644 index 0000000000..ea2292dda5 --- /dev/null +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -0,0 +1,108 @@ +require "cases/helper" + +module ActiveRecord + class Migration + class CommandRecorderTest < ActiveRecord::TestCase + def setup + @recorder = CommandRecorder.new + end + + def test_respond_to_delegates + recorder = CommandRecorder.new(Class.new { + def america; end + }.new) + assert recorder.respond_to?(:america) + end + + def test_send_calls_super + assert_raises(NoMethodError) do + @recorder.send(:create_table, :horses) + end + end + + def test_send_delegates_to_record + recorder = CommandRecorder.new(Class.new { + def create_table(name); end + }.new) + assert recorder.respond_to?(:create_table), 'respond_to? create_table' + recorder.send(:create_table, :horses) + assert_equal [[:create_table, [:horses]]], recorder.commands + end + + def test_unknown_commands_raise_exception + @recorder.record :execute, ['some sql'] + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse + end + end + + def test_record + @recorder.record :create_table, [:system_settings] + assert_equal 1, @recorder.commands.length + end + + def test_inverse + @recorder.record :create_table, [:system_settings] + assert_equal 1, @recorder.inverse.length + + @recorder.record :rename_table, [:old, :new] + assert_equal 2, @recorder.inverse.length + end + + def test_inverted_commands_are_reveresed + @recorder.record :create_table, [:hello] + @recorder.record :create_table, [:world] + tables = @recorder.inverse.map(&:last) + assert_equal [[:world], [:hello]], tables + end + + def test_invert_create_table + @recorder.record :create_table, [:system_settings] + drop_table = @recorder.inverse.first + assert_equal [:drop_table, [:system_settings]], drop_table + end + + def test_invert_rename_table + @recorder.record :rename_table, [:old, :new] + rename = @recorder.inverse.first + assert_equal [:rename_table, [:new, :old]], rename + end + + def test_invert_add_column + @recorder.record :add_column, [:table, :column, :type, {}] + remove = @recorder.inverse.first + assert_equal [:remove_column, [:table, :column]], remove + end + + def test_invert_rename_column + @recorder.record :rename_column, [:table, :old, :new] + rename = @recorder.inverse.first + assert_equal [:rename_column, [:table, :new, :old]], rename + end + + def test_invert_add_index + @recorder.record :add_index, [:table, [:one, :two], {:options => true}] + remove = @recorder.inverse.first + assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove + end + + def test_invert_rename_index + @recorder.record :rename_index, [:old, :new] + rename = @recorder.inverse.first + assert_equal [:rename_index, [:new, :old]], rename + end + + def test_invert_add_timestamps + @recorder.record :add_timestamps, [:table] + remove = @recorder.inverse.first + assert_equal [:remove_timestamps, [:table]], remove + end + + def test_invert_remove_timestamps + @recorder.record :remove_timestamps, [:table] + add = @recorder.inverse.first + assert_equal [:add_timestamps, [:table]], add + end + end + end +end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index e6eef805cf..3037d73a1b 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -18,10 +18,11 @@ if ActiveRecord::Base.connection.supports_migrations? class ActiveRecord::Migration class <<self attr_accessor :message_count - def puts(text="") - self.message_count ||= 0 - self.message_count += 1 - end + end + + def puts(text="") + self.class.message_count ||= 0 + self.class.message_count += 1 end end @@ -1165,6 +1166,44 @@ if ActiveRecord::Base.connection.supports_migrations? assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } end + class MockMigration < ActiveRecord::Migration + attr_reader :went_up, :went_down + def initialize + @went_up = false + @went_down = false + end + + def up + @went_up = true + super + end + + def down + @went_down = true + super + end + end + + def test_instance_based_migration_up + migration = MockMigration.new + assert !migration.went_up, 'have not gone up' + assert !migration.went_down, 'have not gone down' + + migration.migrate :up + assert migration.went_up, 'have gone up' + assert !migration.went_down, 'have not gone down' + end + + def test_instance_based_migration_down + migration = MockMigration.new + assert !migration.went_up, 'have not gone up' + assert !migration.went_down, 'have not gone down' + + migration.migrate :down + assert !migration.went_up, 'have gone up' + assert migration.went_down, 'have not gone down' + end + def test_migrator_one_up assert !Person.column_methods_hash.include?(:last_name) assert !Reminder.table_exists? @@ -1312,20 +1351,20 @@ if ActiveRecord::Base.connection.supports_migrations? def test_migrator_verbosity ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) - assert PeopleHaveLastNames.message_count > 0 + assert_operator PeopleHaveLastNames.message_count, :>, 0 PeopleHaveLastNames.message_count = 0 ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 0) - assert PeopleHaveLastNames.message_count > 0 + assert_operator PeopleHaveLastNames.message_count, :>, 0 PeopleHaveLastNames.message_count = 0 end def test_migrator_verbosity_off PeopleHaveLastNames.verbose = false ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) - assert PeopleHaveLastNames.message_count.zero? + assert_equal 0, PeopleHaveLastNames.message_count ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 0) - assert PeopleHaveLastNames.message_count.zero? + assert_equal 0, PeopleHaveLastNames.message_count end def test_migrator_going_down_due_to_version_target @@ -1947,7 +1986,7 @@ if ActiveRecord::Base.connection.supports_migrations? @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] - Time.travel_to(created_at = Time.utc(2010, 7, 26, 10, 10, 10)) do + Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb") assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb") @@ -1972,7 +2011,7 @@ if ActiveRecord::Base.connection.supports_migrations? sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps2" - Time.travel_to(created_at = Time.utc(2010, 7, 26, 10, 10, 10)) do + Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, sources) assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb") assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb") @@ -1992,7 +2031,7 @@ if ActiveRecord::Base.connection.supports_migrations? @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] - Time.travel_to(created_at = Time.utc(2010, 2, 20, 10, 10, 10)) do + Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.rb") assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.rb") @@ -2024,11 +2063,26 @@ if ActiveRecord::Base.connection.supports_migrations? clear end + def test_copying_migrations_to_non_existing_directory + @migrations_path = MIGRATIONS_ROOT + "/non_existing" + @existing_migrations = [] + + Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) + assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb") + assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb") + assert_equal 2, copied.length + end + ensure + clear + Dir.delete(@migrations_path) + end + def test_copying_migrations_to_empty_directory @migrations_path = MIGRATIONS_ROOT + "/empty" @existing_migrations = [] - Time.travel_to(created_at = Time.utc(2010, 7, 26, 10, 10, 10)) do + Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb") assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb") diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index fb24c65fff..6ac3e3fc56 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -122,7 +122,7 @@ class NamedScopeTest < ActiveRecord::TestCase :joins => 'JOIN authors ON authors.id = posts.author_id', :conditions => [ 'authors.author_address_id = ?', address.id ] ) - assert_equal posts_with_authors_at_address_titles, Post.with_authors_at_address(address).find(:all, :select => 'title') + assert_equal posts_with_authors_at_address_titles.map(&:title), Post.with_authors_at_address(address).find(:all, :select => 'title').map(&:title) end def test_scope_with_object diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 60f89da95e..fb6a239545 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -139,6 +139,14 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert_equal 'gardening', interest.reload.topic end + def test_reject_if_with_blank_nested_attributes_id + # When using a select list to choose an existing 'ship' id, with :include_blank => true + Pirate.accepts_nested_attributes_for :ship, :reject_if => proc {|attributes| attributes[:id].blank? } + + pirate = Pirate.new(:catchphrase => "Stop wastin' me time") + pirate.ship_attributes = { :id => "" } + assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.save! } + end end class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase @@ -163,7 +171,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase @ship.destroy @pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' } - assert @pirate.ship.new_record? + assert !@pirate.ship.persisted? assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name end @@ -184,7 +192,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_replace_an_existing_record_if_there_is_no_id @pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' } - assert @pirate.ship.new_record? + assert !@pirate.ship.persisted? assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name assert_equal 'Nights Dirty Lightning', @ship.name end @@ -256,7 +264,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_also_work_with_a_HashWithIndifferentAccess @pirate.ship_attributes = HashWithIndifferentAccess.new(:id => @ship.id, :name => 'Davy Jones Gold Dagger') - assert !@pirate.ship.new_record? + assert @pirate.ship.persisted? assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name end @@ -348,7 +356,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase @pirate.destroy @ship.reload.pirate_attributes = { :catchphrase => 'Arr' } - assert @ship.pirate.new_record? + assert !@ship.pirate.persisted? assert_equal 'Arr', @ship.pirate.catchphrase end @@ -369,7 +377,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_replace_an_existing_record_if_there_is_no_id @ship.reload.pirate_attributes = { :catchphrase => 'Arr' } - assert @ship.pirate.new_record? + assert !@ship.pirate.persisted? assert_equal 'Arr', @ship.pirate.catchphrase assert_equal 'Aye', @pirate.catchphrase end @@ -458,7 +466,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase @pirate.delete @ship.reload.attributes = { :update_only_pirate_attributes => { :catchphrase => 'Arr' } } - assert @ship.update_only_pirate.new_record? + assert !@ship.update_only_pirate.persisted? end def test_should_update_existing_when_update_only_is_true_and_no_id_is_given @@ -596,10 +604,10 @@ module NestedAttributesOnACollectionAssociationTests association_getter => { 'foo' => { :name => 'Grace OMalley' }, 'bar' => { :name => 'Privateers Greed' }} } - assert @pirate.send(@association_name).first.new_record? + assert !@pirate.send(@association_name).first.persisted? assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name - assert @pirate.send(@association_name).last.new_record? + assert !@pirate.send(@association_name).last.persisted? assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name end @@ -819,7 +827,7 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase fixtures :owners, :pets def setup - Owner.accepts_nested_attributes_for :pets + Owner.accepts_nested_attributes_for :pets, :allow_destroy => true @owner = owners(:ashley) @pet1, @pet2 = pets(:chew), pets(:mochi) @@ -836,6 +844,19 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase @owner.update_attributes(@params) assert_equal ['Foo', 'Bar'], @owner.pets.map(&:name) end + + def test_attr_accessor_of_child_should_be_value_provided_during_update_attributes + @owner = owners(:ashley) + @pet1 = pets(:chew) + assert_equal nil, $current_user + attributes = {:pets_attributes => { "1"=> { :id => @pet1.id, + :name => "Foo2", + :current_user => "John", + :_destroy=>true }}} + @owner.update_attributes(attributes) + assert_equal 'John', $after_destroy_callback_output + end + end class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index ffe6fb95b8..8ca9d626d1 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -192,7 +192,6 @@ class PersistencesTest < ActiveRecord::TestCase topic = Topic.create("title" => "New Topic") do |t| t.author_name = "David" end - topicReloaded = Topic.find(topic.id) assert_equal("New Topic", topic.title) assert_equal("David", topic.author_name) end @@ -241,6 +240,15 @@ class PersistencesTest < ActiveRecord::TestCase assert_nothing_raised { minimalistic.save } end + def test_update_sti_type + assert_instance_of Reply, topics(:second) + + topic = topics(:second).becomes(Topic) + assert_instance_of Topic, topic + topic.save! + assert_instance_of Topic, Topic.find(topic.id) + end + def test_delete topic = Topic.find(1) assert_equal topic, topic.delete, 'topic.delete did not return self' @@ -261,7 +269,7 @@ class PersistencesTest < ActiveRecord::TestCase end def test_record_not_found_exception - assert_raise(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(99999) } end def test_update_all diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 594db1d0ab..33916c4e46 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -22,6 +22,12 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_find_queries_with_cache_multi_record + Task.cache do + assert_queries(2) { Task.find(1); Task.find(1); Task.find(2) } + end + end + def test_count_queries_with_cache Task.cache do assert_queries(1) { Task.count; Task.count } @@ -57,7 +63,7 @@ class QueryCacheTest < ActiveRecord::TestCase # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter) && SQLite3::Version::VERSION > '1.2.5' or current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:SQLite3Adapter) && SQLite3::Version::VERSION > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") @@ -127,7 +133,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_cache_is_expired_by_habtm_delete ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) ActiveRecord::Base.cache do - c = Category.find(1) p = Post.find(1) assert p.categories.any? p.categories.delete_all diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index eeb619ac2f..3b9e4f42a6 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 group 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 created_at updated_at ).sort, @first.attribute_names ) end def test_columns - assert_equal 14, Topic.columns.length + assert_equal 16, 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 group), 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 created_at updated_at), column_names end def test_content_columns content_columns = Topic.content_columns content_column_names = content_columns.map {|column| column.name} - 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 + assert_equal 12, content_columns.length + assert_equal %w(title author_name author_email_address written_on bonus_time last_read content group approved parent_title created_at updated_at).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 index a27e2e72cd..dae9721a63 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -254,13 +254,11 @@ class HasManyScopingTest< ActiveRecord::TestCase end def test_should_maintain_default_scope_on_associations - person = people(:michael) magician = BadReference.find(1) assert_equal [magician], people(:michael).bad_references end def test_should_default_scope_on_associations_is_overriden_by_association_conditions - person = people(:michael) assert_equal [], people(:michael).fixed_bad_references end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index b01a8bbef1..1682f34a1d 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'models/tag' require 'models/tagging' require 'models/post' require 'models/topic' @@ -13,11 +14,27 @@ require 'models/bird' require 'models/car' require 'models/engine' require 'models/tyre' +require 'models/minivan' class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, - :taggings, :cars + :tags, :taggings, :cars, :minivans + + def test_do_not_double_quote_string_id + van = Minivan.last + assert van + assert_equal van.id, Minivan.where(:minivan_id => van).to_a.first.minivan_id + end + + def test_bind_values + relation = Post.scoped + assert_equal [], relation.bind_values + + relation2 = relation.bind 'foo' + assert_equal %w{ foo }, relation2.bind_values + assert_equal [], relation.bind_values + end def test_two_named_scopes_with_includes_should_not_drop_any_include car = Car.incl_engines.incl_tyres.first @@ -135,6 +152,16 @@ class RelationTest < ActiveRecord::TestCase assert_equal entrants(:first).name, entrants.first.name end + def test_finding_with_complex_order_and_limit + tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a + assert_equal 1, tags.length + end + + def test_finding_with_complex_order + tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a + assert_equal 2, tags.length + end + def test_finding_with_order_limit_and_offset entrants = Entrant.order("id ASC").limit(2).offset(1) @@ -363,7 +390,7 @@ class RelationTest < ActiveRecord::TestCase lifo = authors.find_or_initialize_by_name('Lifo') assert_equal "Lifo", lifo.name - assert lifo.new_record? + assert !lifo.persisted? assert_equal authors(:david), authors.find_or_initialize_by_name(:name => 'David') end @@ -373,7 +400,7 @@ class RelationTest < ActiveRecord::TestCase lifo = authors.find_or_create_by_name('Lifo') assert_equal "Lifo", lifo.name - assert ! lifo.new_record? + assert lifo.persisted? assert_equal authors(:david), authors.find_or_create_by_name(:name => 'David') end @@ -406,6 +433,52 @@ class RelationTest < ActiveRecord::TestCase assert_blank authors.all end + def test_where_with_ar_object + author = Author.first + authors = Author.scoped.where(:id => author) + assert_equal 1, authors.all.length + end + + def test_find_with_list_of_ar + author = Author.first + authors = Author.find([author]) + assert_equal author, authors.first + end + + class Mary < Author; end + + def test_find_by_classname + Author.create!(:name => Mary.name) + assert_equal 1, Author.where(:name => Mary).size + end + + def test_find_by_id_with_list_of_ar + author = Author.first + authors = Author.find_by_id([author]) + assert_equal author, authors + end + + def test_find_all_using_where_twice_should_or_the_relation + david = authors(:david) + relation = Author.unscoped + relation = relation.where(:name => david.name) + relation = relation.where(:name => 'Santiago') + relation = relation.where(:id => david.id) + assert_equal [david], relation.all + end + + def test_find_all_with_multiple_ors + david = authors(:david) + relation = [ + { :name => david.name }, + { :name => 'Santiago' }, + { :name => 'tenderlove' }, + ].inject(Author.unscoped) do |memo, param| + memo.where(param) + end + assert_equal [david], relation.all + end + def test_exists davids = Author.where(:name => 'David') assert davids.exists? @@ -491,6 +564,11 @@ class RelationTest < ActiveRecord::TestCase end end + def test_relation_merging_with_joins + comments = Comment.joins(:post).where(:body => 'Thank you for the welcome') & Post.where(:body => 'Such a lovely day') + assert_equal 1, comments.count + end + def test_count posts = Post.scoped @@ -602,10 +680,10 @@ class RelationTest < ActiveRecord::TestCase sparrow = birds.create assert_kind_of Bird, sparrow - assert sparrow.new_record? + assert !sparrow.persisted? hen = birds.where(:name => 'hen').create - assert ! hen.new_record? + assert hen.persisted? assert_equal 'hen', hen.name end @@ -616,7 +694,7 @@ class RelationTest < ActiveRecord::TestCase hen = birds.where(:name => 'hen').create! assert_kind_of Bird, hen - assert ! hen.new_record? + assert hen.persisted? assert_equal 'hen', hen.name end diff --git a/activerecord/test/cases/session_store/sql_bypass.rb b/activerecord/test/cases/session_store/sql_bypass.rb index f0ba166465..7402b2afd6 100644 --- a/activerecord/test/cases/session_store/sql_bypass.rb +++ b/activerecord/test/cases/session_store/sql_bypass.rb @@ -18,9 +18,9 @@ module ActiveRecord assert !Session.table_exists? end - def test_new_record? + def test_persisted? s = SqlBypass.new :data => 'foo', :session_id => 10 - assert s.new_record?, 'this is a new record!' + assert !s.persisted?, 'this is a new record!' end def test_not_loaded? diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index eb93761fb2..70c098bc6d 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -113,7 +113,7 @@ class TimestampTest < ActiveRecord::TestCase pet = Pet.first owner = pet.owner - owner.update_attribute(:happy_at, (time = 3.days.ago)) + owner.update_attribute(:happy_at, 3.days.ago) previously_owner_updated_at = owner.updated_at pet.name = "I'm a parrot" diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 0fbcef4091..b0ccd71836 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -163,7 +163,7 @@ class TransactionTest < ActiveRecord::TestCase @first.author_name += '_this_should_not_end_up_in_the_db' @first.save! flunk - rescue => e + rescue assert_equal original_author_name, @first.reload.author_name assert_equal nbooks_before_save, Book.count ensure @@ -182,7 +182,7 @@ class TransactionTest < ActiveRecord::TestCase :bonus_time => "2005-01-30t15:28:00.00+01:00", :content => "Have a nice day", :approved => false) - new_record_snapshot = new_topic.new_record? + new_record_snapshot = !new_topic.persisted? id_present = new_topic.has_attribute?(Topic.primary_key) id_snapshot = new_topic.id @@ -195,7 +195,7 @@ class TransactionTest < ActiveRecord::TestCase flunk rescue => e assert_equal "Make the transaction rollback", e.message - assert_equal new_record_snapshot, new_topic.new_record?, "The topic should have its old new_record value" + assert_equal new_record_snapshot, !new_topic.persisted?, "The topic should have its old persisted value" assert_equal id_snapshot, new_topic.id, "The topic should have its old id" assert_equal id_present, new_topic.has_attribute?(Topic.primary_key) ensure @@ -370,21 +370,21 @@ class TransactionTest < ActiveRecord::TestCase assert topic_2.save @first.save @second.destroy - assert_equal false, topic_1.new_record? + assert_equal true, topic_1.persisted? assert_not_nil topic_1.id - assert_equal false, topic_2.new_record? + assert_equal true, topic_2.persisted? assert_not_nil topic_2.id - assert_equal false, @first.new_record? + assert_equal true, @first.persisted? assert_not_nil @first.id assert_equal true, @second.destroyed? raise ActiveRecord::Rollback end - assert_equal true, topic_1.new_record? + assert_equal false, topic_1.persisted? assert_nil topic_1.id - assert_equal true, topic_2.new_record? + assert_equal false, topic_2.persisted? assert_nil topic_2.id - assert_equal false, @first.new_record? + assert_equal true, @first.persisted? assert_not_nil @first.id assert_equal false, @second.destroyed? end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index 1246dd4276..56e345990f 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -17,7 +17,7 @@ class AssociationValidationTest < ActiveRecord::TestCase o = Owner.new('name' => 'nopets') assert !o.save assert o.errors[:pets].any? - pet = o.pets.build('name' => 'apet') + o.pets.build('name' => 'apet') assert o.valid? end @@ -27,7 +27,7 @@ class AssociationValidationTest < ActiveRecord::TestCase assert !o.save assert o.errors[:pets].any? - pet = o.pets.build('name' => 'apet') + o.pets.build('name' => 'apet') assert o.valid? 2.times { o.pets.build('name' => 'apet') } diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 9a863c25a8..679d67553b 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -60,7 +60,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase def test_validates_uniqueness_with_validates Topic.validates :title, :uniqueness => true - t = Topic.create!('title' => 'abc') + Topic.create!('title' => 'abc') t2 = Topic.new('title' => 'abc') assert !t2.valid? @@ -201,7 +201,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer Topic.validates_uniqueness_of(:title, :case_sensitve => true) - t = Topic.create!('title' => 101) + Topic.create!('title' => 101) t2 = Topic.new('title' => 101) assert !t2.valid? diff --git a/activerecord/test/connections/native_oracle/connection.rb b/activerecord/test/connections/native_oracle/connection.rb index 9a717018b2..99f921879c 100644 --- a/activerecord/test/connections/native_oracle/connection.rb +++ b/activerecord/test/connections/native_oracle/connection.rb @@ -33,15 +33,3 @@ ActiveRecord::Base.configurations = { ActiveRecord::Base.establish_connection 'arunit' Course.establish_connection 'arunit2' -# for assert_queries test helper -ActiveRecord::Base.connection.class.class_eval do - IGNORED_SELECT_SQL = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from ((all|user)_tab_columns|(all|user)_triggers|(all|user)_constraints)/im] - - def select_with_query_record(sql, name = nil, return_column_names = false) - $queries_executed ||= [] - $queries_executed << sql unless IGNORED_SELECT_SQL.any? { |r| sql =~ r } - select_without_query_record(sql, name, return_column_names) - end - - alias_method_chain :select, :query_record -end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index be6dd71e3b..ee5f77b613 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -107,6 +107,7 @@ class Client < Company belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of" belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1] belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name" + belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true # Record destruction so we can test whether firm.clients.clear has diff --git a/activerecord/test/models/eye.rb b/activerecord/test/models/eye.rb index 77f17b578e..dc8ae2b3f6 100644 --- a/activerecord/test/models/eye.rb +++ b/activerecord/test/models/eye.rb @@ -17,7 +17,7 @@ class Eye < ActiveRecord::Base after_save :trace_after_save2 def trace_after_create - (@after_create_callbacks_stack ||= []) << iris.new_record? + (@after_create_callbacks_stack ||= []) << !iris.persisted? end alias trace_after_create2 trace_after_create diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index a8bf94dd86..570db4c8d5 100644 --- a/activerecord/test/models/pet.rb +++ b/activerecord/test/models/pet.rb @@ -1,5 +1,13 @@ class Pet < ActiveRecord::Base + + attr_accessor :current_user + set_primary_key :pet_id belongs_to :owner, :touch => true has_many :toys + + after_destroy do |record| + $after_destroy_callback_output = record.current_user + end + end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index d89c8cf381..f2c45053e7 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -48,7 +48,7 @@ class Pirate < ActiveRecord::Base end def reject_empty_ships_on_create(attributes) - attributes.delete('_reject_me_if_new').present? && new_record? + attributes.delete('_reject_me_if_new').present? && !persisted? end attr_accessor :cancel_save_from_callback diff --git a/activerecord/test/models/subject.rb b/activerecord/test/models/subject.rb index d4b8b91de8..8e28f8b86b 100644 --- a/activerecord/test/models/subject.rb +++ b/activerecord/test/models/subject.rb @@ -8,7 +8,7 @@ class Subject < ActiveRecord::Base protected def set_email_address - if self.new_record? + unless self.persisted? self.author_email_address = 'test@test.com' end end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 82d4b5997f..6496f36f7e 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -89,7 +89,7 @@ class Topic < ActiveRecord::Base end def set_email_address - if self.new_record? + unless self.persisted? self.author_email_address = 'test@test.com' end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index bb80a1ca17..d4eb56c4d7 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -552,6 +552,7 @@ ActiveRecord::Schema.define do t.string :parent_title t.string :type t.string :group + t.timestamps end create_table :toys, :primary_key => :toy_id ,:force => true do |t| diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 24fe3072e0..386bafd7de 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -2,6 +2,10 @@ * No changes +*Rails 3.0.2 (unreleased)* + +* No changes + *Rails 3.0.1 (October 15, 2010)* * No Changes, just a version bump. diff --git a/activeresource/Rakefile b/activeresource/Rakefile index 905241acc0..cf01bc1389 100644..100755 --- a/activeresource/Rakefile +++ b/activeresource/Rakefile @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rake/gempackagetask' diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index b5b46d7431..d959fd103a 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -1,6 +1,6 @@ require 'active_support' require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/module/attr_accessor_with_default' @@ -263,6 +263,8 @@ module ActiveResource # The logger for diagnosing and tracing Active Resource calls. cattr_accessor :logger + class_attribute :_format + class << self # Creates a schema for this resource - setting the attributes that are # known prior to fetching an instance from the remote system. @@ -492,13 +494,13 @@ module ActiveResource format = mime_type_reference_or_format.is_a?(Symbol) ? ActiveResource::Formats[mime_type_reference_or_format] : mime_type_reference_or_format - write_inheritable_attribute(:format, format) + self._format = format connection.format = format if site end # Returns the current format, default is ActiveResource::Formats::XmlFormat. def format - read_inheritable_attribute(:format) || ActiveResource::Formats::XmlFormat + self._format || ActiveResource::Formats::XmlFormat end # Sets the number of seconds after which requests to the REST API should time out. diff --git a/activeresource/lib/active_resource/version.rb b/activeresource/lib/active_resource/version.rb index e2e0ecccbb..82dcb5d575 100644 --- a/activeresource/lib/active_resource/version.rb +++ b/activeresource/lib/active_resource/version.rb @@ -3,8 +3,8 @@ module ActiveResource MAJOR = 3 MINOR = 1 TINY = 0 - BUILD = "beta" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/activeresource/test/cases/base_test.rb b/activeresource/test/cases/base_test.rb index abf4259a54..ab801902ac 100644 --- a/activeresource/test/cases/base_test.rb +++ b/activeresource/test/cases/base_test.rb @@ -463,9 +463,9 @@ class BaseTest < Test::Unit::TestCase assert Person.collection_path(:gender => 'male', :student => true).include?('gender=male') assert Person.collection_path(:gender => 'male', :student => true).include?('student=true') - assert_equal '/people.xml?name[]=bob&name[]=your+uncle%2Bme&name[]=&name[]=false', Person.collection_path(:name => ['bob', 'your uncle+me', nil, false]) + assert_equal '/people.xml?name%5B%5D=bob&name%5B%5D=your+uncle%2Bme&name%5B%5D=&name%5B%5D=false', Person.collection_path(:name => ['bob', 'your uncle+me', nil, false]) - assert_equal '/people.xml?struct[a][]=2&struct[a][]=1&struct[b]=fred', Person.collection_path(:struct => ActiveSupport::OrderedHash[:a, [2,1], 'b', 'fred']) + assert_equal '/people.xml?struct%5Ba%5D%5B%5D=2&struct%5Ba%5D%5B%5D=1&struct%5Bb%5D=fred', Person.collection_path(:struct => ActiveSupport::OrderedHash[:a, [2,1], 'b', 'fred']) end def test_custom_element_path @@ -512,7 +512,7 @@ class BaseTest < Test::Unit::TestCase assert_equal '/people/1/addresses/1.xml?type=work', StreetAddress.element_path(1, :person_id => 1, :type => 'work') assert_equal '/people/1/addresses/1.xml?type=work', StreetAddress.element_path(1, 'person_id' => 1, :type => 'work') assert_equal '/people/1/addresses/1.xml?type=work', StreetAddress.element_path(1, :type => 'work', :person_id => 1) - assert_equal '/people/1/addresses/1.xml?type[]=work&type[]=play+time', StreetAddress.element_path(1, :person_id => 1, :type => ['work', 'play time']) + assert_equal '/people/1/addresses/1.xml?type%5B%5D=work&type%5B%5D=play+time', StreetAddress.element_path(1, :person_id => 1, :type => ['work', 'play time']) end def test_custom_element_path_with_prefix_and_parameters diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 3b6a29f7d6..6e8cce0d27 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -4,6 +4,10 @@ * Added before_remove_const callback to ActiveSupport::Dependencies.remove_unloadable_constants! [Andrew White] +*Rails 3.0.2 (unreleased)* + +* Added before_remove_const callback to ActiveSupport::Dependencies.remove_unloadable_constants! [Andrew White] + *Rails 3.0.1 (October 15, 2010)* * No Changes, just a version bump. diff --git a/activesupport/Rakefile b/activesupport/Rakefile index d117ca6356..d117ca6356 100644..100755 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 9771ca966f..b4f0c42e37 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -210,8 +210,8 @@ module ActiveSupport # be specified as an option to the construction in which call all entries will be # affected. Or it can be supplied to the +fetch+ or +write+ method for just one entry. # - # cache = ActiveSupport::Cache::MemoryStore.new(:expire_in => 5.minutes) - # cache.write(key, value, :expire_in => 1.minute) # Set a lower value for one entry + # cache = ActiveSupport::Cache::MemoryStore.new(:expires_in => 5.minutes) + # cache.write(key, value, :expires_in => 1.minute) # Set a lower value for one entry # # Setting <tt>:race_condition_ttl</tt> is very useful in situations where a cache entry # is used very frequently and is under heavy load. If a cache expires and due to heavy load diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 84f6f29572..18182bbb40 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/file/atomic' require 'active_support/core_ext/string/conversions' +require 'rack/utils' module ActiveSupport module Cache @@ -11,8 +12,6 @@ module ActiveSupport attr_reader :cache_path DIR_FORMATTER = "%03X" - ESCAPE_FILENAME_CHARS = /[^a-z0-9_.-]/i - UNESCAPE_FILENAME_CHARS = /%[0-9A-F]{2}/ def initialize(cache_path, options = nil) super(options) @@ -136,7 +135,7 @@ module ActiveSupport # Translate a key into a file path. def key_file_path(key) - fname = key.to_s.gsub(ESCAPE_FILENAME_CHARS){|match| "%#{match.ord.to_s(16).upcase}"} + fname = Rack::Utils.escape(key) hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) dir_2 = hash.modulo(0x1000) @@ -156,7 +155,7 @@ module ActiveSupport # Translate a file path into a key. def file_path_key(path) fname = path[cache_path.size, path.size].split(File::SEPARATOR, 4).last - fname.gsub(UNESCAPE_FILENAME_CHARS){|match| $1.ord.to_s(16)} + Rack::Utils.unescape(fname) end # Delete empty directories in the cache. diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 8f8def5922..905dfb040b 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/attribute' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' @@ -437,7 +437,7 @@ module ActiveSupport ([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target| chain = target.send("_#{name}_callbacks") - yield chain, type, filters, options + yield target, chain.dup, type, filters, options target.__define_runner(name) end end @@ -473,7 +473,7 @@ module ActiveSupport def set_callback(name, *filter_list, &block) mapped = nil - __update_callbacks(name, filter_list, block) do |chain, type, filters, options| + __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options| mapped ||= filters.map do |filter| Callback.new(chain, filter, type, options.dup, self) end @@ -483,6 +483,8 @@ module ActiveSupport end options[:prepend] ? chain.unshift(*(mapped.reverse)) : chain.push(*mapped) + + target.send("_#{name}_callbacks=", chain) end end @@ -493,7 +495,7 @@ module ActiveSupport # end # def skip_callback(name, *filter_list, &block) - __update_callbacks(name, filter_list, block) do |chain, type, filters, options| + __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options| filters.each do |filter| filter = chain.find {|c| c.matches?(type, filter) } @@ -505,6 +507,7 @@ module ActiveSupport chain.delete(filter) end + target.send("_#{name}_callbacks=", chain) end end @@ -514,12 +517,14 @@ module ActiveSupport callbacks = send("_#{symbol}_callbacks") ActiveSupport::DescendantsTracker.descendants(self).each do |target| - chain = target.send("_#{symbol}_callbacks") + chain = target.send("_#{symbol}_callbacks").dup callbacks.each { |c| chain.delete(c) } + target.send("_#{symbol}_callbacks=", chain) target.__define_runner(symbol) end - callbacks.clear + self.send("_#{symbol}_callbacks=", callbacks.dup.clear) + __define_runner(symbol) end @@ -589,9 +594,8 @@ module ActiveSupport def define_callbacks(*callbacks) config = callbacks.last.is_a?(Hash) ? callbacks.pop : {} callbacks.each do |callback| - extlib_inheritable_reader("_#{callback}_callbacks") do - CallbackChain.new(callback, config) - end + class_attribute "_#{callback}_callbacks" + send("_#{callback}_callbacks=", CallbackChain.new(callback, config)) __define_runner(callback) end end diff --git a/activesupport/lib/active_support/core_ext/cgi.rb b/activesupport/lib/active_support/core_ext/cgi.rb deleted file mode 100644 index 7279a3d4da..0000000000 --- a/activesupport/lib/active_support/core_ext/cgi.rb +++ /dev/null @@ -1 +0,0 @@ -require 'active_support/core_ext/cgi/escape_skipping_slashes' diff --git a/activesupport/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb b/activesupport/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb deleted file mode 100644 index d3c3575748..0000000000 --- a/activesupport/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'cgi' - -class CGI #:nodoc: - if RUBY_VERSION >= '1.9' - def self.escape_skipping_slashes(str) - str = str.join('/') if str.respond_to? :join - str.gsub(/([^ \/a-zA-Z0-9_.-])/n) do - "%#{$1.unpack('H2' * $1.bytesize).join('%').upcase}" - end.tr(' ', '+') - end - else - def self.escape_skipping_slashes(str) - str = str.join('/') if str.respond_to? :join - str.gsub(/([^ \/a-zA-Z0-9_.-])/n) do - "%#{$1.unpack('H2').first.upcase}" - end.tr(' ', '+') - end - end -end 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 af30bfc13a..ca3db2349e 100644 --- a/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb @@ -1,8 +1,10 @@ require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/array/extract_options' +require 'active_support/deprecation' # Retained for backward compatibility. Methods are now included in Class. module ClassInheritableAttributes # :nodoc: + DEPRECATION_WARNING_MESSAGE = "class_inheritable_attribute is deprecated, please use class_attribute method instead. Notice their behavior are slightly different, so refer to class_attribute documentation first" end # It is recommended to use <tt>class_attribute</tt> over methods defined in this file. Please @@ -36,6 +38,7 @@ end # Person.new.hair_colors # => NoMethodError class Class # :nodoc: def class_inheritable_reader(*syms) + ActiveSupport::Deprecation.warn ClassInheritableAttributes::DEPRECATION_WARNING_MESSAGE options = syms.extract_options! syms.each do |sym| next if sym.is_a?(Hash) @@ -54,6 +57,7 @@ class Class # :nodoc: end def class_inheritable_writer(*syms) + ActiveSupport::Deprecation.warn ClassInheritableAttributes::DEPRECATION_WARNING_MESSAGE options = syms.extract_options! syms.each do |sym| class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@ -71,6 +75,7 @@ class Class # :nodoc: end def class_inheritable_array_writer(*syms) + ActiveSupport::Deprecation.warn ClassInheritableAttributes::DEPRECATION_WARNING_MESSAGE options = syms.extract_options! syms.each do |sym| class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@ -88,6 +93,7 @@ class Class # :nodoc: end def class_inheritable_hash_writer(*syms) + ActiveSupport::Deprecation.warn ClassInheritableAttributes::DEPRECATION_WARNING_MESSAGE options = syms.extract_options! syms.each do |sym| class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@ -124,6 +130,7 @@ class Class # :nodoc: end def write_inheritable_attribute(key, value) + ActiveSupport::Deprecation.warn ClassInheritableAttributes::DEPRECATION_WARNING_MESSAGE if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES) @inheritable_attributes = {} end @@ -141,10 +148,12 @@ class Class # :nodoc: end def read_inheritable_attribute(key) + ActiveSupport::Deprecation.warn ClassInheritableAttributes::DEPRECATION_WARNING_MESSAGE inheritable_attributes[key] end def reset_inheritable_attributes + ActiveSupport::Deprecation.warn ClassInheritableAttributes::DEPRECATION_WARNING_MESSAGE @inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES end @@ -169,86 +178,3 @@ class Class # :nodoc: alias inherited_without_inheritable_attributes inherited alias inherited inherited_with_inheritable_attributes end - -class Class - # Defines class-level inheritable attribute reader. Attributes are available to subclasses, - # each subclass has a copy of parent's attribute. - # - # @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for. - # @return <Array[#to_s]> Array of attributes converted into inheritable_readers. - # - # @api public - # - # @todo Do we want to block instance_reader via :instance_reader => false - # @todo It would be preferable that we do something with a Hash passed in - # (error out or do the same as other methods above) instead of silently - # moving on). In particular, this makes the return value of this function - # less useful. - def extlib_inheritable_reader(*ivars, &block) - options = ivars.extract_options! - - ivars.each do |ivar| - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def self.#{ivar} - return @#{ivar} if self.object_id == #{self.object_id} || defined?(@#{ivar}) - ivar = superclass.#{ivar} - return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}") - @#{ivar} = ivar.duplicable? ? ivar.dup : ivar - end - RUBY - unless options[:instance_reader] == false - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{ivar} - self.class.#{ivar} - end - RUBY - end - instance_variable_set(:"@#{ivar}", yield) if block_given? - end - end - - # Defines class-level inheritable attribute writer. Attributes are available to subclasses, - # each subclass has a copy of parent's attribute. - # - # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to - # define inheritable writer for. - # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined. - # @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers. - # - # @api public - # - # @todo We need a style for class_eval <<-HEREDOC. I'd like to make it - # class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere. - def extlib_inheritable_writer(*ivars) - options = ivars.extract_options! - - ivars.each do |ivar| - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def self.#{ivar}=(obj) - @#{ivar} = obj - end - RUBY - unless options[:instance_writer] == false - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{ivar}=(obj) self.class.#{ivar} = obj end - RUBY - end - - self.send("#{ivar}=", yield) if block_given? - end - end - - # Defines class-level inheritable attribute accessor. Attributes are available to subclasses, - # each subclass has a copy of parent's attribute. - # - # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to - # define inheritable accessor for. - # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined. - # @return <Array[#to_s]> An Array of attributes turned into inheritable accessors. - # - # @api public - def extlib_inheritable_accessor(*syms, &block) - extlib_inheritable_reader(*syms) - extlib_inheritable_writer(*syms, &block) - end -end diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index 501483498d..fd1cda991e 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/deep_merge' +require 'active_support/core_ext/hash/deep_dup' require 'active_support/core_ext/hash/diff' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/indifferent_access' diff --git a/activesupport/lib/active_support/core_ext/hash/deep_dup.rb b/activesupport/lib/active_support/core_ext/hash/deep_dup.rb new file mode 100644 index 0000000000..447142605c --- /dev/null +++ b/activesupport/lib/active_support/core_ext/hash/deep_dup.rb @@ -0,0 +1,11 @@ +class Hash + # Returns a deep copy of hash. + def deep_dup + duplicate = self.dup + duplicate.each_pair do |k,v| + tv = duplicate[k] + duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_dup : v + end + duplicate + end +end diff --git a/activesupport/lib/active_support/core_ext/module/synchronization.rb b/activesupport/lib/active_support/core_ext/module/synchronization.rb index 38ce55f26e..ed16c2f71b 100644 --- a/activesupport/lib/active_support/core_ext/module/synchronization.rb +++ b/activesupport/lib/active_support/core_ext/module/synchronization.rb @@ -1,3 +1,4 @@ +require 'thread' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/array/extract_options' diff --git a/activesupport/lib/active_support/core_ext/object/instance_variables.rb b/activesupport/lib/active_support/core_ext/object/instance_variables.rb index 77a3cfc21d..eda9694614 100644 --- a/activesupport/lib/active_support/core_ext/object/instance_variables.rb +++ b/activesupport/lib/active_support/core_ext/object/instance_variables.rb @@ -30,35 +30,4 @@ class Object else alias_method :instance_variable_names, :instance_variables end - - # Copies the instance variables of +object+ into +self+. - # - # Instance variable names in the +exclude+ array are ignored. If +object+ - # responds to <tt>protected_instance_variables</tt> the ones returned are - # also ignored. For example, Rails controllers implement that method. - # - # In both cases strings and symbols are understood, and they have to include - # the at sign. - # - # class C - # def initialize(x, y, z) - # @x, @y, @z = x, y, z - # end - # - # def protected_instance_variables - # %w(@z) - # end - # end - # - # a = C.new(0, 1, 2) - # b = C.new(3, 4, 5) - # - # a.copy_instance_variables_from(b, [:@y]) - # # a is now: @x = 3, @y = 1, @z = 2 - def copy_instance_variables_from(object, exclude = []) #:nodoc: - exclude += object.protected_instance_variables if object.respond_to? :protected_instance_variables - - vars = object.instance_variables.map(&:to_s) - exclude.map(&:to_s) - vars.each { |name| instance_variable_set(name, object.instance_variable_get(name)) } - end end 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 ecb2bca82c..593f376159 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -1,5 +1,3 @@ - - class Object # Alias of <tt>to_s</tt>. def to_param @@ -41,7 +39,7 @@ class Hash # ==== Examples # { :name => 'David', :nationality => 'Danish' }.to_param # => "name=David&nationality=Danish" # - # { :name => 'David', :nationality => 'Danish' }.to_param('user') # => "user[name]=David&user[nationality]=Danish" + # { :name => 'David', :nationality => 'Danish' }.to_query('user') # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish" def to_param(namespace = nil) collect do |key, value| value.to_query(namespace ? "#{namespace}[#{key}]" : key) diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index c9981895b4..3f1540f685 100644 --- a/activesupport/lib/active_support/core_ext/object/to_query.rb +++ b/activesupport/lib/active_support/core_ext/object/to_query.rb @@ -7,7 +7,7 @@ class Object # Note: This method is defined as a default implementation for all Objects for Hash#to_query to work. def to_query(key) require 'cgi' unless defined?(CGI) && defined?(CGI::escape) - "#{CGI.escape(key.to_s).gsub(/%(5B|5D)/n) { [$1].pack('H*') }}=#{CGI.escape(to_param.to_s)}" + "#{CGI.escape(key.to_s)}=#{CGI.escape(to_param.to_s)}" end end @@ -15,7 +15,7 @@ class Array # Converts an array into a string suitable for use as a URL query string, # using the given +key+ as the param name. # - # ['Rails', 'coding'].to_query('hobbies') # => "hobbies[]=Rails&hobbies[]=coding" + # ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding" def to_query(key) prefix = "#{key}[]" collect { |value| value.to_query(prefix) }.join '&' diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 48e9d04787..4d742b9ed2 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -8,6 +8,8 @@ class Object # *Unlike* that method however, a +NoMethodError+ exception will *not* be raised # and +nil+ will be returned instead, if the receiving object is a +nil+ object or NilClass. # + # If try is called without a method to call, it will yield any given block with the object. + # # ==== Examples # # Without try @@ -21,10 +23,20 @@ class Object # +try+ also accepts arguments and/or a block, for the method it is trying # Person.try(:find, 1) # @people.try(:collect) {|p| p.name} + # + # Without a method argument try will yield to the block unless the reciever is nil. + # @person.try { |p| "#{p.first_name} #{p.last_name}" } #-- # +try+ behaves like +Object#send+, unless called on +NilClass+. - alias_method :try, :__send__ + def try(*a, &b) + if a.empty? && block_given? + yield self + else + __send__(*a, &b) + end + end + end class NilClass #:nodoc: diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 37c206ea3c..bb0f747960 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -33,21 +33,23 @@ class ERB singleton_class.send(:remove_method, :html_escape) module_function :html_escape - # A utility method for escaping HTML entities in JSON strings. - # This method is also aliased as <tt>j</tt>. + # A utility method for escaping HTML entities in JSON strings + # using \uXXXX JavaScript escape sequences for string literals: # - # Note that after this operation is performed the output is not - # a valid JSON. + # json_escape("is a > 0 & a < 10?") + # # => is a \u003E 0 \u0026 a \u003C 10? # - # In your ERb templates, use this method to escape any HTML entities: - # <%=j @person.to_json %> + # Note that after this operation is performed the output is not + # valid JSON. In particular double quotes are removed: # - # ==== Example: - # puts json_escape("{\"name\":\"john\",\"created_at\":\"2010-04-28T01:39:31Z\",\"id\":1}") + # json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}') # # => {name:john,created_at:2010-04-28T01:39:31Z,id:1} # - # puts json_escape("is a > 0 & a < 10?") - # # => is a \u003E 0 \u0026 a \u003C 10? + # This method is also aliased as +j+, and available as a helper + # in Rails templates: + # + # <%=j @person.to_json %> + # def json_escape(s) s.to_s.gsub(/[&"><]/) { |special| JSON_ESCAPE[special] } end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index c406dd3c2e..6a344867ee 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -28,7 +28,7 @@ module ActiveSupport end def self.new_from_hash_copying_default(hash) - ActiveSupport::HashWithIndifferentAccess.new(hash).tap do |new_hash| + new(hash).tap do |new_hash| new_hash.default = hash.default end end @@ -97,7 +97,9 @@ module ActiveSupport # Returns an exact copy of the hash. def dup - HashWithIndifferentAccess.new(self) + self.class.new(self).tap do |new_hash| + new_hash.default = default + end end # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash @@ -138,11 +140,10 @@ module ActiveSupport end def convert_value(value) - case value - when Hash + if value.class == Hash self.class.new_from_hash_copying_default(value) - when Array - value.collect { |e| e.is_a?(Hash) ? self.class.new_from_hash_copying_default(e) : e } + elsif value.is_a?(Array) + value.dup.replace(value.map { |e| convert_value(e) }) else value end diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index 4d33f597d9..00ea8813dd 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -7,4 +7,3 @@ rescue LoadError => e end I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml" -ActiveSupport.run_load_hooks(:i18n) diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 7f148dc853..8e157d3af4 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -137,6 +137,8 @@ module ActiveSupport alias_method :each_pair, :each + alias_method :select, :find_all + def clear super @keys.clear diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 93f5d5a0cc..6a7da8266c 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -73,7 +73,7 @@ module ActiveSupport # Returns a <tt>Time.local()</tt> instance of the simultaneous time in your system's <tt>ENV['TZ']</tt> zone def localtime - utc.getlocal + utc.respond_to?(:getlocal) ? utc.getlocal : utc.to_time.getlocal end alias_method :getlocal, :localtime diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index 9e8b3d7888..690fc7f0fc 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -3,8 +3,8 @@ module ActiveSupport MAJOR = 3 MINOR = 1 TINY = 0 - BUILD = "beta" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index b6a8cf3caf..cddfcddb57 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -126,9 +126,11 @@ module ActiveSupport end def rename_key(key, options = {}) - camelize = options.has_key?(:camelize) && options[:camelize] + camelize = options[:camelize] dasherize = !options.has_key?(:dasherize) || options[:dasherize] - key = key.camelize if camelize + if camelize + key = true == camelize ? key.camelize : key.camelize(camelize) + end key = _dasherize(key) if dasherize key end diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb index 38c8685390..25afbfcd1c 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -38,8 +38,7 @@ module ActiveSupport end def start_element(name, attrs = []) - new_hash = { CONTENT_KEY => '' } - new_hash[attrs.shift] = attrs.shift while attrs.length > 0 + new_hash = { CONTENT_KEY => '' }.merge(Hash[attrs]) new_hash[HASH_SIZE_KEY] = new_hash.size + 1 case current_hash[name] diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 28ef695a4b..579d5dad24 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -356,9 +356,13 @@ module CacheDeleteMatchedBehavior def test_delete_matched @cache.write("foo", "bar") @cache.write("fu", "baz") + @cache.write("foo/bar", "baz") + @cache.write("fu/baz", "bar") @cache.delete_matched(/oo/) assert_equal false, @cache.exist?("foo") assert_equal true, @cache.exist?("fu") + assert_equal false, @cache.exist?("foo/bar") + assert_equal true, @cache.exist?("fu/baz") end end @@ -509,6 +513,11 @@ class FileStoreTest < ActiveSupport::TestCase assert_nil old_cache.read('foo') end end + + def test_key_transformation + key = @cache.send(:key_file_path, "views/index?id=1") + assert_equal "views/index?id=1", @cache.send(:file_path_key, key) + end end class MemoryStoreTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/cgi_ext_test.rb b/activesupport/test/core_ext/cgi_ext_test.rb deleted file mode 100644 index c80362e382..0000000000 --- a/activesupport/test/core_ext/cgi_ext_test.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/cgi' - -class EscapeSkippingSlashesTest < Test::Unit::TestCase - def test_array - assert_equal 'hello/world', CGI.escape_skipping_slashes(%w(hello world)) - assert_equal 'hello+world/how/are/you', CGI.escape_skipping_slashes(['hello world', 'how', 'are', 'you']) - end - - def test_typical - assert_equal 'hi', CGI.escape_skipping_slashes('hi') - assert_equal 'hi/world', CGI.escape_skipping_slashes('hi/world') - assert_equal 'hi/world+you+funky+thing', CGI.escape_skipping_slashes('hi/world you funky thing') - end -end diff --git a/activesupport/test/core_ext/class/class_inheritable_attributes_test.rb b/activesupport/test/core_ext/class/class_inheritable_attributes_test.rb index b284e5ee1c..020dfce56a 100644 --- a/activesupport/test/core_ext/class/class_inheritable_attributes_test.rb +++ b/activesupport/test/core_ext/class/class_inheritable_attributes_test.rb @@ -3,9 +3,14 @@ require 'active_support/core_ext/class/inheritable_attributes' class ClassInheritableAttributesTest < Test::Unit::TestCase def setup + ActiveSupport::Deprecation.silenced = true @klass = Class.new end + def teardown + ActiveSupport::Deprecation.silenced = false + end + def test_reader_declaration assert_nothing_raised do @klass.class_inheritable_reader :a diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 545fed2684..74223dd7f2 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -6,6 +6,15 @@ require 'active_support/ordered_hash' require 'active_support/core_ext/object/conversions' class HashExtTest < Test::Unit::TestCase + class IndifferentHash < HashWithIndifferentAccess + end + + class SubclassingArray < Array + end + + class SubclassingHash < Hash + end + def setup @strings = { 'a' => 1, 'b' => 2 } @symbols = { :a => 1, :b => 2 } @@ -99,6 +108,11 @@ class HashExtTest < Test::Unit::TestCase assert_equal @strings, @mixed.with_indifferent_access.dup.stringify_keys! end + def test_hash_subclass + flash = { "foo" => SubclassingHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access + assert_kind_of SubclassingHash, flash["foo"] + end + def test_indifferent_assorted @strings = @strings.with_indifferent_access @symbols = @symbols.with_indifferent_access @@ -248,6 +262,20 @@ class HashExtTest < Test::Unit::TestCase hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] }}.with_indifferent_access assert_equal "1", hash[:urls][:url].first[:address] end + + def test_should_preserve_array_subclass_when_value_is_array + array = SubclassingArray.new + array << { "address" => "1" } + hash = { "urls" => { "url" => array }}.with_indifferent_access + assert_equal SubclassingArray, hash[:urls][:url].class + end + + def test_should_preserve_array_class_when_hash_value_is_frozen_array + array = SubclassingArray.new + array << { "address" => "1" } + hash = { "urls" => { "url" => array.freeze }}.with_indifferent_access + assert_equal SubclassingArray, hash[:urls][:url].class + end def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash h = HashWithIndifferentAccess.new @@ -267,7 +295,6 @@ class HashExtTest < Test::Unit::TestCase assert_equal 1, h['first'] end - def test_indifferent_subhashes h = {'user' => {'id' => 5}}.with_indifferent_access ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}} @@ -276,6 +303,17 @@ class HashExtTest < Test::Unit::TestCase ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}} end + def test_indifferent_duplication + # Should preserve default value + h = HashWithIndifferentAccess.new + h.default = '1234' + assert_equal h.default, h.dup.default + + # Should preserve class for subclasses + h = IndifferentHash.new + assert_equal h.class, h.dup.class + end + def test_assert_valid_keys assert_nothing_raised do { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ]) @@ -316,6 +354,21 @@ class HashExtTest < Test::Unit::TestCase assert_equal expected, hash_1 end + def test_deep_dup + hash = { :a => { :b => 'b' } } + dup = hash.deep_dup + dup[:a][:c] = 'c' + assert_equal nil, hash[:a][:c] + assert_equal 'c', dup[:a][:c] + end + + def test_deep_dup_initialize + zero_hash = Hash.new 0 + hash = { :a => zero_hash } + dup = hash.deep_dup + assert_equal 0, dup[:a][44] + end + def test_store_on_indifferent_access hash = HashWithIndifferentAccess.new hash.store(:test1, 1) @@ -486,7 +539,7 @@ class HashExtToParamTests < Test::Unit::TestCase def test_to_param_hash_escapes_its_keys_and_values assert_equal 'param+1=A+string+with+%2F+characters+%26+that+should+be+%3F+escaped', { 'param 1' => 'A string with / characters & that should be ? escaped' }.to_param end - + def test_to_param_orders_by_key_in_ascending_order assert_equal 'a=2&b=1&c=0', ActiveSupport::OrderedHash[*%w(b 1 c 0 a 2)].to_param end @@ -525,6 +578,13 @@ class HashToXmlTest < Test::Unit::TestCase assert xml.include?(%(<Name>David</Name>)) end + def test_one_level_camelize_lower + xml = { :name => "David", :street_name => "Paulina" }.to_xml(@xml_options.merge(:camelize => :lower)) + assert_equal "<person>", xml.first(8) + assert xml.include?(%(<streetName>Paulina</streetName>)) + assert xml.include?(%(<name>David</name>)) + end + def test_one_level_with_types xml = { :name => "David", :street => "Paulina", :age => 26, :age_in_millis => 820497600000, :moved_on => Date.new(2005, 11, 15), :resident => :yes }.to_xml(@xml_options) assert_equal "<person>", xml.first(8) diff --git a/activesupport/test/core_ext/module/synchronization_test.rb b/activesupport/test/core_ext/module/synchronization_test.rb index eb850893f0..6c407e2260 100644 --- a/activesupport/test/core_ext/module/synchronization_test.rb +++ b/activesupport/test/core_ext/module/synchronization_test.rb @@ -1,3 +1,4 @@ +require 'thread' require 'abstract_unit' require 'active_support/core_ext/class/attribute_accessors' diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index e28b4cd493..84da52f4bf 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -18,22 +18,22 @@ class ToQueryTest < Test::Unit::TestCase end def test_nested_conversion - assert_query_equal 'person[login]=seckar&person[name]=Nicholas', + assert_query_equal 'person%5Blogin%5D=seckar&person%5Bname%5D=Nicholas', :person => ActiveSupport::OrderedHash[:login, 'seckar', :name, 'Nicholas'] end def test_multiple_nested - assert_query_equal 'account[person][id]=20&person[id]=10', + assert_query_equal 'account%5Bperson%5D%5Bid%5D=20&person%5Bid%5D=10', ActiveSupport::OrderedHash[:account, {:person => {:id => 20}}, :person, {:id => 10}] end def test_array_values - assert_query_equal 'person[id][]=10&person[id][]=20', + assert_query_equal 'person%5Bid%5D%5B%5D=10&person%5Bid%5D%5B%5D=20', :person => {:id => [10, 20]} end def test_array_values_are_not_sorted - assert_query_equal 'person[id][]=20&person[id][]=10', + assert_query_equal 'person%5Bid%5D%5B%5D=20&person%5Bid%5D%5B%5D=10', :person => {:id => [20, 10]} end diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb index 3ccf18f473..398e6ca9b2 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object_and_class_ext_test.rb @@ -82,37 +82,6 @@ class ObjectInstanceVariableTest < Test::Unit::TestCase assert_equal %w(@bar @baz), @source.instance_variable_names.sort end - def test_copy_instance_variables_from_without_explicit_excludes - assert_equal [], @dest.instance_variables - @dest.copy_instance_variables_from(@source) - - assert_equal %w(@bar @baz), @dest.instance_variables.sort.map(&:to_s) - %w(@bar @baz).each do |name| - assert_equal @source.instance_variable_get(name).object_id, - @dest.instance_variable_get(name).object_id - end - end - - def test_copy_instance_variables_from_with_explicit_excludes - @dest.copy_instance_variables_from(@source, ['@baz']) - assert !@dest.instance_variable_defined?('@baz') - assert_equal 'bar', @dest.instance_variable_get('@bar') - end - - def test_copy_instance_variables_automatically_excludes_protected_instance_variables - @source.instance_variable_set(:@quux, 'quux') - class << @source - def protected_instance_variables - ['@bar', :@quux] - end - end - - @dest.copy_instance_variables_from(@source) - assert !@dest.instance_variable_defined?('@bar') - assert !@dest.instance_variable_defined?('@quux') - assert_equal 'baz', @dest.instance_variable_get('@baz') - end - def test_instance_values object = Object.new object.instance_variable_set :@a, 1 @@ -165,4 +134,14 @@ class ObjectTryTest < Test::Unit::TestCase def test_false_try assert_equal 'false', false.try(:to_s) end + + def test_try_only_block + assert_equal @string.reverse, @string.try { |s| s.reverse } + end + + def test_try_only_block_nil + ran = false + nil.try { ran = true } + assert_equal false, ran + end end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 8be65c99f2..bb865cae91 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -321,75 +321,6 @@ class CoreExtStringMultibyteTest < ActiveSupport::TestCase end end -=begin - string.rb - Interpolation for String. - - Copyright (C) 2005-2009 Masao Mutoh - - You may redistribute it and/or modify it under the same - license terms as Ruby. -=end -class TestGetTextString < Test::Unit::TestCase - def test_sprintf - assert_equal("foo is a number", "%{msg} is a number" % {:msg => "foo"}) - assert_equal("bar is a number", "%s is a number" % ["bar"]) - assert_equal("bar is a number", "%s is a number" % "bar") - assert_equal("1, test", "%{num}, %{record}" % {:num => 1, :record => "test"}) - assert_equal("test, 1", "%{record}, %{num}" % {:num => 1, :record => "test"}) - assert_equal("1, test", "%d, %s" % [1, "test"]) - assert_equal("test, 1", "%2$s, %1$d" % [1, "test"]) - assert_raise(ArgumentError) { "%-%" % [1] } - end - - def test_percent - assert_equal("% 1", "%% %<num>d" % {:num => 1.0}) - assert_equal("%{num} %<num>d 1", "%%{num} %%<num>d %<num>d" % {:num => 1}) - end - - def test_sprintf_percent_in_replacement - assert_equal("%<not_translated>s", "%{msg}" % { :msg => '%<not_translated>s', :not_translated => 'should not happen' }) - end - - def test_sprintf_lack_argument - assert_raises(KeyError) { "%{num}, %{record}" % {:record => "test"} } - assert_raises(KeyError) { "%{record}" % {:num => 1} } - end - - def test_no_placeholder - # Causes a "too many arguments for format string" warning - # on 1.8.7 and 1.9 but we still want to make sure the behavior works - silence_warnings do - assert_equal("aaa", "aaa" % {:num => 1}) - assert_equal("bbb", "bbb" % [1]) - end - end - - def test_sprintf_ruby19_style - assert_equal("1", "%<num>d" % {:num => 1}) - assert_equal("0b1", "%<num>#b" % {:num => 1}) - assert_equal("foo", "%<msg>s" % {:msg => "foo"}) - assert_equal("1.000000", "%<num>f" % {:num => 1.0}) - assert_equal(" 1", "%<num>3.0f" % {:num => 1.0}) - assert_equal("100.00", "%<num>2.2f" % {:num => 100.0}) - assert_equal("0x64", "%<num>#x" % {:num => 100.0}) - assert_raise(ArgumentError) { "%<num>,d" % {:num => 100} } - assert_raise(ArgumentError) { "%<num>/d" % {:num => 100} } - end - - def test_sprintf_old_style - assert_equal("foo 1.000000", "%s %f" % ["foo", 1.0]) - end - - def test_sprintf_mix_unformatted_and_formatted_named_placeholders - assert_equal("foo 1.000000", "%{name} %<num>f" % {:name => "foo", :num => 1.0}) - end - - def test_string_interpolation_raises_an_argument_error_when_mixing_named_and_unnamed_placeholders - assert_raises(ArgumentError) { "%{name} %f" % [1.0] } - assert_raises(ArgumentError) { "%{name} %f" % [1.0, 2.0] } - end -end - class OutputSafetyTest < ActiveSupport::TestCase def setup @string = "hello" diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 0bb2c4a39e..5579c27215 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -36,6 +36,10 @@ class TimeWithZoneTest < Test::Unit::TestCase assert_equal @twz.object_id, @twz.in_time_zone(ActiveSupport::TimeZone['Eastern Time (US & Canada)']).object_id end + def test_localtime + assert_equal @twz.localtime, @twz.utc.getlocal + end + def test_utc? assert_equal false, @twz.utc? assert_equal true, ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']).utc? @@ -763,6 +767,13 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase end end + def test_localtime + Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + assert_equal @dt.in_time_zone.localtime, @dt.in_time_zone.utc.to_time.getlocal + ensure + Time.zone_default = nil + end + def test_use_zone Time.zone = 'Alaska' Time.use_zone 'Hawaii' do diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb index 50778b5864..72088854fc 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -109,6 +109,14 @@ class OrderedHashTest < Test::Unit::TestCase assert_equal @keys, keys end + def test_find_all + assert_equal @keys, @ordered_hash.find_all { true }.map(&:first) + end + + def test_select + assert_equal @keys, @ordered_hash.select { true }.map(&:first) + end + def test_delete_if copy = @ordered_hash.dup copy.delete('pink') diff --git a/activesupport/test/test_xml_mini.rb b/activesupport/test/test_xml_mini.rb index 585eb15c6e..309fa234bf 100644 --- a/activesupport/test/test_xml_mini.rb +++ b/activesupport/test/test_xml_mini.rb @@ -14,14 +14,26 @@ class XmlMiniTest < Test::Unit::TestCase assert_equal "my_key", ActiveSupport::XmlMini.rename_key("my_key", :dasherize => false) end - def test_rename_key_camelizes_with_camelize_true - assert_equal "MyKey", ActiveSupport::XmlMini.rename_key("my_key", :camelize => true) + def test_rename_key_camelizes_with_camelize_false + assert_equal "my_key", ActiveSupport::XmlMini.rename_key("my_key", :camelize => false) + end + + def test_rename_key_camelizes_with_camelize_nil + assert_equal "my_key", ActiveSupport::XmlMini.rename_key("my_key", :camelize => nil) end def test_rename_key_camelizes_with_camelize_true assert_equal "MyKey", ActiveSupport::XmlMini.rename_key("my_key", :camelize => true) end + def test_rename_key_lower_camelizes_with_camelize_lower + assert_equal "myKey", ActiveSupport::XmlMini.rename_key("my_key", :camelize => :lower) + end + + def test_rename_key_lower_camelizes_with_camelize_upper + assert_equal "MyKey", ActiveSupport::XmlMini.rename_key("my_key", :camelize => :upper) + end + def test_rename_key_does_not_dasherize_leading_underscores assert_equal "_id", ActiveSupport::XmlMini.rename_key("_id") end diff --git a/rails.gemspec b/rails.gemspec index d791e21ec2..98b5f46554 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -25,5 +25,5 @@ Gem::Specification.new do |s| s.add_dependency('activeresource', version) s.add_dependency('actionmailer', version) s.add_dependency('railties', version) - s.add_dependency('bundler', '~> 1.0.3') + s.add_dependency('bundler', '~> 1.0') end diff --git a/railties/CHANGELOG b/railties/CHANGELOG index fdfdb5eef5..75f1df44e7 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.1.0 (unreleased)* +* Added `rails plugin new` command which generates rails plugin with gemspec, tests and dummy application for testing [Piotr Sarnacki] + * Added -j parameter with jquery/prototype as options. Now you can create your apps with jQuery using `rails new myapp -j jquery`. The default is still Prototype. [siong1987] * Added Rack::Etag and Rack::ConditionalGet to the default middleware stack [José Valim] @@ -20,7 +22,7 @@ * Changed ActionDispatch::Static to allow handling multiple directories [Piotr Sarnacki] -* Added namespace() method to Engine, which sets Engine as isolated [Piotr Sarnacki] +* Added isolate_namespace() method to Engine, which sets Engine as isolated [Piotr Sarnacki] * Include all helpers from plugins and shared engines in application [Piotr Sarnacki] diff --git a/railties/Rakefile b/railties/Rakefile index 9504661c0e..5137bee761 100644..100755 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/gempackagetask' diff --git a/railties/guides/assets/stylesheets/main.css b/railties/guides/assets/stylesheets/main.css index 611fb6b7e0..cf6e9e5cc8 100644 --- a/railties/guides/assets/stylesheets/main.css +++ b/railties/guides/assets/stylesheets/main.css @@ -23,8 +23,12 @@ dl { margin: 0 0 1.5em 0; } dl dt { font-weight: bold; } dd { margin-left: 1.5em;} -pre,code { margin: 1.5em 0; overflow: auto; color: #333;} -pre,code,tt { font: 1em 'Anonymous Pro', 'Inconsolata', 'Menlo', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; line-height: 1.5; } +pre,code { margin: 1.5em 0; overflow: auto; color: #222;} +pre,code,tt { + font-size: 1em; + font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + line-height: 1.5; +} abbr, acronym { border-bottom: 1px dotted #666; } address { margin: 0 0 1.5em; font-style: italic; } @@ -329,7 +333,7 @@ h6 { padding: 0.125em 0 0.25em 28px;*/ } -#mainCol dd.ticket, #subCol dd.ticket { +#mainCol dd.work-in-progress, #subCol dd.work-in-progress { background: #fff9d8 url(../images/tab_yellow.gif) no-repeat left top; border: none; padding: 1.25em 1em 1.25em 48px; @@ -361,10 +365,6 @@ h6 { font-weight: normal; } -tt { - font-family: monaco, "Bitstream Vera Sans Mono", "Courier New", courier, monospace; -} - div.code_container { background: #EEE url(../images/tab_grey.gif) no-repeat left top; padding: 0.25em 1em 0.5em 48px; diff --git a/railties/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css b/railties/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css index c36a4bd2ba..6d2edb2eb8 100644 --- a/railties/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css +++ b/railties/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css @@ -3,6 +3,9 @@ */ .syntaxhighlighter { background-color: #eee !important; + font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace !important; + overflow-y: hidden !important; + overflow-x: auto !important; } .syntaxhighlighter .line.alt1 { background-color: #eee !important; @@ -17,7 +20,7 @@ color: #eee !important; } .syntaxhighlighter table caption { - color: #333 !important; + color: #222 !important; } .syntaxhighlighter .gutter { color: #787878 !important; @@ -58,7 +61,7 @@ color: red !important; } .syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: #333 !important; + color: #222 !important; } .syntaxhighlighter .comments, .syntaxhighlighter .comments a { color: #708090 !important; @@ -74,7 +77,7 @@ color: #646464 !important; } .syntaxhighlighter .variable { - color: #333 !important; + color: #222 !important; } .syntaxhighlighter .value { color: #009900 !important; @@ -86,14 +89,14 @@ color: #0066cc !important; } .syntaxhighlighter .script { - color: #333 !important; + color: #222 !important; background-color: none !important; } .syntaxhighlighter .color1, .syntaxhighlighter .color1 a { color: gray !important; } .syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #333 !important; + color: #222 !important; font-weight: bold !important; } .syntaxhighlighter .color3, .syntaxhighlighter .color3 a { diff --git a/railties/guides/rails_guides/generator.rb b/railties/guides/rails_guides/generator.rb index 22d402ad21..0a2170a09e 100644 --- a/railties/guides/rails_guides/generator.rb +++ b/railties/guides/rails_guides/generator.rb @@ -223,8 +223,8 @@ module RailsGuides code_blocks.push(<<HTML) <notextile> <div class="code_container"> -<pre class="brush: #{brush}; gutter: false"> -#{ERB::Util.h($2).chomp} +<pre class="brush: #{brush}; gutter: false; toolbar: false"> +#{ERB::Util.h($2).strip} </pre> </div> </notextile> diff --git a/railties/guides/rails_guides/helpers.rb b/railties/guides/rails_guides/helpers.rb index bf99538696..d466c76c7c 100644 --- a/railties/guides/rails_guides/helpers.rb +++ b/railties/guides/rails_guides/helpers.rb @@ -4,19 +4,14 @@ module RailsGuides link = content_tag(:a, :href => url) { name } result = content_tag(:dt, link) - if ticket = options[:ticket] - result << content_tag(:dd, lh(ticket), :class => 'ticket') + if options[:work_in_progress] + result << content_tag(:dd, 'Work in progress', :class => 'work-in-progress') end result << content_tag(:dd, capture(&block)) result end - def lh(id, label = "Lighthouse Ticket") - url = "http://rails.lighthouseapp.com/projects/16213/tickets/#{id}" - content_tag(:a, label, :href => url) - end - def author(name, nick, image = 'credits_pic_blank.gif', &block) image = "images/#{image}" diff --git a/railties/guides/source/3_0_release_notes.textile b/railties/guides/source/3_0_release_notes.textile index 9c08c9fa0a..adb1c755df 100644 --- a/railties/guides/source/3_0_release_notes.textile +++ b/railties/guides/source/3_0_release_notes.textile @@ -271,10 +271,10 @@ end <ruby> scope 'es' do - resources :projects, :path_names => { :edit => 'cambiar' }, :path => 'projeto' + resources :projects, :path_names => { :edit => 'cambiar' }, :path => 'proyecto' end -# Gives you the edit action with /es/projeto/1/cambiar +# Gives you the edit action with /es/proyecto/1/cambiar </ruby> * Added +root+ method to the router as a short cut for <tt>match '/', :to => path</tt>. diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile index c02e9f1912..0d6c66f168 100644 --- a/railties/guides/source/action_controller_overview.textile +++ b/railties/guides/source/action_controller_overview.textile @@ -239,7 +239,7 @@ class LoginsController < ApplicationController # "Delete" a login, aka "log the user out" def destroy # Remove the user id from the session - session[:current_user_id] = nil + @_current_user = session[:current_user_id] = nil redirect_to root_url end end @@ -261,6 +261,13 @@ class LoginsController < ApplicationController end </ruby> +Note it is also possible to assign a flash message as part of the redirection. + +<ruby> +redirect_to root_url, :notice => "You have successfully logged out" +</ruby> + + The +destroy+ action redirects to the application's +root_url+, where the message will be displayed. Note that it's entirely up to the next action to decide what, if anything, it will do with what the previous action put in the flash. It's conventional to display eventual errors or notices from the flash in the application's layout: <ruby> @@ -807,8 +814,6 @@ NOTE: Certain exceptions are only rescuable from the +ApplicationController+ cla h3. Changelog -"Lighthouse Ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/17 - * February 17, 2009: Yet another proofread by Xavier Noria. * November 4, 2008: First release version by Tore Darell diff --git a/railties/guides/source/action_mailer_basics.textile b/railties/guides/source/action_mailer_basics.textile index 2259061c30..b75c528a33 100644 --- a/railties/guides/source/action_mailer_basics.textile +++ b/railties/guides/source/action_mailer_basics.textile @@ -446,7 +446,7 @@ The following configuration options are best made in one of the environment file |sendmail_settings|Allows you to override options for the :sendmail delivery method.<ul><li>:location - The location of the sendmail executable. Defaults to /usr/sbin/sendmail.</li><li>:arguments - The command line arguments to be passed to sendmail. Defaults to -i -t.</li></ul>| |raise_delivery_errors|Whether or not errors should be raised if the email fails to be delivered.| |delivery_method|Defines a delivery method. Possible values are :smtp (default), :sendmail, :file and :test.| -|perform_deliveries|Determines whether deliver_* methods are actually carried out. By default they are, but this can be turned off to help functional testing.| +|perform_deliveries|Determines whether deliveries are actually carried out when the +deliver+ method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing.| |deliveries|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.| h4. Example Action Mailer Configuration @@ -492,7 +492,7 @@ class UserMailerTest < ActionMailer::TestCase user = users(:some_user_in_your_fixtures) # Send the email, then test that it got queued - email = UserMailer.deliver_welcome_email(user) + email = UserMailer.welcome_email(user).deliver assert !ActionMailer::Base.deliveries.empty? # Test the body of the sent email contains what we expect it to @@ -508,6 +508,4 @@ In the test we send the email and store the returned object in the +email+ varia h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/25 - * September 30, 2010: Fixed typos and reformatted Action Mailer configuration table for better understanding. "Jaime Iniesta":http://jaimeiniesta.com diff --git a/railties/guides/source/action_view_overview.textile b/railties/guides/source/action_view_overview.textile index 0e1a352ebd..051baa660c 100644 --- a/railties/guides/source/action_view_overview.textile +++ b/railties/guides/source/action_view_overview.textile @@ -233,7 +233,7 @@ h4. AssetTagHelper This module provides methods for generating HTML that links views to assets such as images, javascripts, stylesheets, and feeds. -By default, Rails links to these assets on the current host in the public folder, but you can direct Rails to link to assets from a dedicated assets server by setting +ActionController::Base.asset_host+ in your +config/environment.rb+. For example, let's say your asset host is +assets.example.com+: +By default, Rails links to these assets on the current host in the public folder, but you can direct Rails to link to assets from a dedicated assets server by setting +ActionController::Base.asset_host+ in the application configuration, typically in +config/environments/production.rb+. For example, let's say your asset host is +assets.example.com+: <ruby> ActionController::Base.asset_host = "assets.example.com" @@ -1472,7 +1472,5 @@ You can read more about the Rails Internationalization (I18n) API "here":i18n.ht h3. Changelog -"Lighthouse Ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/71 - * September 3, 2009: Continuing work by Trevor Turk, leveraging the "Action Pack docs":http://ap.rubyonrails.org/ and "What's new in Edge Rails":http://ryandaigle.com/articles/2007/8/3/what-s-new-in-edge-rails-partials-get-layouts * April 5, 2009: Starting work by Trevor Turk, leveraging Mike Gunderloy's docs diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile index 6837c8b11a..e41b5fb606 100644 --- a/railties/guides/source/active_record_querying.textile +++ b/railties/guides/source/active_record_querying.textile @@ -65,6 +65,7 @@ The methods are: * +lock+ * +readonly+ * +from+ +* +having+ All of the above methods return an instance of <tt>ActiveRecord::Relation</tt>. @@ -103,7 +104,7 @@ h5. +first+ <ruby> client = Client.first -=> #<Client id: 1, first_name: => "Lifo"> +=> #<Client id: 1, first_name: "Lifo"> </ruby> SQL equivalent of the above is: @@ -120,7 +121,7 @@ h5. +last+ <ruby> client = Client.last -=> #<Client id: 221, first_name: => "Russel"> +=> #<Client id: 221, first_name: "Russel"> </ruby> SQL equivalent of the above is: @@ -231,7 +232,7 @@ WARNING: Building your own conditions as pure strings can leave you vulnerable t h4. Array Conditions -Now what if that number could vary, say as an argument from somewhere, or perhaps from the user's level status somewhere? The find then becomes something like: +Now what if that number could vary, say as an argument from somewhere? The find then becomes something like: <ruby> Client.where("orders_count = ?", params[:orders]) @@ -279,62 +280,15 @@ h5(#array-range_conditions). Range Conditions If you're looking for a range inside of a table (for example, users created in a certain timeframe) you can use the conditions option coupled with the +IN+ SQL statement for this. If you had two dates coming in from a controller you could do something like this to look for a range: <ruby> -Client.where("created_at IN (?)", - (params[:start_date].to_date)..(params[:end_date].to_date)) +Client.where(:created_at => (params[:start_date].to_date)..(params[:end_date].to_date)) </ruby> -This would generate the proper query which is great for small ranges but not so good for larger ranges. For example if you pass in a range of date objects spanning a year that's 365 (or possibly 366, depending on the year) strings it will attempt to match your field against. +This query will generate something similar to the following SQL: <sql> -SELECT * FROM users WHERE (created_at IN - ('2007-12-31','2008-01-01','2008-01-02','2008-01-03','2008-01-04','2008-01-05', - '2008-01-06','2008-01-07','2008-01-08','2008-01-09','2008-01-10','2008-01-11', - '2008-01-12','2008-01-13','2008-01-14','2008-01-15','2008-01-16','2008-01-17', - '2008-01-18','2008-01-19','2008-01-20','2008-01-21','2008-01-22','2008-01-23',... - ‘2008-12-15','2008-12-16','2008-12-17','2008-12-18','2008-12-19','2008-12-20', - '2008-12-21','2008-12-22','2008-12-23','2008-12-24','2008-12-25','2008-12-26', - '2008-12-27','2008-12-28','2008-12-29','2008-12-30','2008-12-31')) + SELECT "clients".* FROM "clients" WHERE ("clients"."created_at" BETWEEN '2010-09-29' AND '2010-11-30') </sql> -h5. Time and Date Conditions - -Things can get *really* messy if you pass in Time objects as it will attempt to compare your field to *every second* in that range: - -<ruby> -Client.where("created_at IN (?)", - (params[:start_date].to_date.to_time)..(params[:end_date].to_date.to_time)) -</ruby> - -<sql> -SELECT * FROM users WHERE (created_at IN - ('2007-12-01 00:00:00', '2007-12-01 00:00:01' ... - '2007-12-01 23:59:59', '2007-12-02 00:00:00')) -</sql> - -This could possibly cause your database server to raise an unexpected error, for example MySQL will throw back this error: - -<shell> -Got a packet bigger than 'max_allowed_packet' bytes: _query_ -</shell> - -Where _query_ is the actual query used to get that error. - -In this example it would be better to use greater-than and less-than operators in SQL, like so: - -<ruby> -Client.where( - "created_at > ? AND created_at < ?", params[:start_date], params[:end_date]) -</ruby> - -You can also use the greater-than-or-equal-to and less-than-or-equal-to like this: - -<ruby> -Client.where( - "created_at >= ? AND created_at <= ?", params[:start_date], params[:end_date]) -</ruby> - -Just like in Ruby. If you want a shorter syntax be sure to check out the "Hash Conditions":#hash-conditions section later on in the guide. - h4. Hash Conditions Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them: @@ -385,7 +339,7 @@ SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5)) h4. Ordering -To retrieve records from the database in a specific order, you can specify the +:order+ option to the +find+ call. +To retrieve records from the database in a specific order, you can use the +order+ method. For example, if you're getting a set of records and want to order them in ascending order by the +created_at+ field in your table: @@ -928,8 +882,6 @@ For options, please see the parent section, "Calculations":#calculations. h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/16 - * April 7, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * February 3, 2010: Update to Rails 3 by "James Miller":credits.html#bensie * February 7, 2009: Second version by "Pratik":credits.html#lifo diff --git a/railties/guides/source/active_record_validations_callbacks.textile b/railties/guides/source/active_record_validations_callbacks.textile index a9d66c0a06..0824ba450c 100644 --- a/railties/guides/source/active_record_validations_callbacks.textile +++ b/railties/guides/source/active_record_validations_callbacks.textile @@ -96,7 +96,7 @@ To verify whether or not an object is valid, Rails uses the +valid?+ method. You <ruby> class Person < ActiveRecord::Base - validates_presence_of :name + validates :name, :presence => true end Person.create(:name => "John Doe").valid? # => true @@ -109,7 +109,7 @@ Note that an object instantiated with +new+ will not report errors even if it's <ruby> class Person < ActiveRecord::Base - validates_presence_of :name + validates :name, :presence => true end >> p = Person.new @@ -141,13 +141,13 @@ end h4(#validations_overview-errors). +errors[]+ -To verify whether or not a particular attribute of an object is valid, you can use +errors[:attribute]+ that returns an array with all attribute errors, when there are no errors on the specified attribute, an empty array is returned. +To verify whether or not a particular attribute of an object is valid, you can use +errors[:attribute]+. It returns an array of all the errors for +:attribute+. If there are no errors on the specified attribute, an empty array is returned. This method is only useful _after_ validations have been run, because it only inspects the errors collection and does not trigger validations itself. It's different from the +ActiveRecord::Base#invalid?+ method explained above because it doesn't verify the validity of the object as a whole. It only checks to see whether there are errors found on an individual attribute of the object. <ruby> class Person < ActiveRecord::Base - validates_presence_of :name + validates :name, :presence => true end >> Person.new.errors[:name].any? # => false @@ -355,7 +355,7 @@ This helper validates that the specified attributes are not empty. It uses the + <ruby> class Person < ActiveRecord::Base - validates_presence_of :name, :login, :email + validates :name, :login, :email, :presence => true end </ruby> @@ -505,7 +505,7 @@ class Person < ActiveRecord::Base validates_numericality_of :age, :on => :update # the default (validates on both create and update) - validates_presence_of :name, :on => :save + validates :name, :presence => true, :on => :save end </ruby> @@ -603,7 +603,7 @@ Returns an OrderedHash with all errors. Each key is the attribute name and the v <ruby> class Person < ActiveRecord::Base - validates_presence_of :name + validates :name, :presence => true validates_length_of :name, :minimum => 3 end @@ -623,7 +623,7 @@ h4(#working_with_validation_errors-errors-2). +errors[]+ <ruby> class Person < ActiveRecord::Base - validates_presence_of :name + validates :name, :presence => true validates_length_of :name, :minimum => 3 end @@ -699,7 +699,7 @@ The +clear+ method is used when you intentionally want to clear all the messages <ruby> class Person < ActiveRecord::Base - validates_presence_of :name + validates :name, :presence => true validates_length_of :name, :minimum => 3 end @@ -723,7 +723,7 @@ The +size+ method returns the total number of error messages for the object. <ruby> class Person < ActiveRecord::Base - validates_presence_of :name + validates :name, :presence => true validates_length_of :name, :minimum => 3 validates_presence_of :email end @@ -824,10 +824,10 @@ Here is a simple example where we change the Rails behaviour to always display t ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| if instance.error_message.kind_of?(Array) %(#{html_tag}<span class="validation-error"> - #{instance.error_message.join(',')}</span>) + #{instance.error_message.join(',')}</span>).html_safe else %(#{html_tag}<span class="validation-error"> - #{instance.error_message}</span>) + #{instance.error_message}</span>).html_safe end end </ruby> @@ -1128,14 +1128,14 @@ As with callback classes, the observer's methods receive the observed model as a h4. Registering Observers -Observers are conventionally placed inside of your +app/models+ directory and registered in your application's +config/environment.rb+ file. For example, the +UserObserver+ above would be saved as +app/models/user_observer.rb+ and registered in +config/environment.rb+ this way: +Observers are conventionally placed inside of your +app/models+ directory and registered in your application's +config/application.rb+ file. For example, the +UserObserver+ above would be saved as +app/models/user_observer.rb+ and registered in +config/application.rb+ this way: <ruby> # Activate observers that should always be running config.active_record.observers = :user_observer </ruby> -As usual, settings in +config/environments+ take precedence over those in +config/environment.rb+. So, if you prefer that an observer doesn't run in all environments, you can simply register it in a specific environment instead. +As usual, settings in +config/environments+ take precedence over those in +config/application.rb+. So, if you prefer that an observer doesn't run in all environments, you can simply register it in a specific environment instead. h4. Sharing Observers @@ -1151,7 +1151,7 @@ class MailerObserver < ActiveRecord::Observer end </ruby> -In this example, the +after_create+ method would be called whenever a +Registration+ or +User+ was created. Note that this new +MailerObserver+ would also need to be registered in +config/environment.rb+ in order to take effect. +In this example, the +after_create+ method would be called whenever a +Registration+ or +User+ was created. Note that this new +MailerObserver+ would also need to be registered in +config/application.rb+ in order to take effect. <ruby> # Activate observers that should always be running @@ -1160,8 +1160,6 @@ config.active_record.observers = :mailer_observer h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/26-active-record-validations-and-callbacks - * July 20, 2010: Fixed typos and rephrased some paragraphs for clarity. "Jaime Iniesta":http://jaimeiniesta.com * May 24, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * May 15, 2010: Validation Errors section updated by "Emili Parreño":http://www.eparreno.com diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index 9b1d264d2c..7333a81cf9 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -167,6 +167,12 @@ def log_info(sql, name, ms) end </ruby> ++try+ can also be called without arguments but a block, which will only be executed if the object is not nil: + +<ruby> +@person.try { |p| "#{p.first_name} #{p.last_name}" } +</ruby> + NOTE: Defined in +active_support/core_ext/object/try.rb+. h4. +singleton_class+ @@ -395,39 +401,6 @@ C.new(0, 1).instance_values # => {"x" => 0, "y" => 1} NOTE: Defined in +active_support/core_ext/object/instance_variables.rb+. -h5. +copy_instance_variables_from(object, exclude = [])+ - -Copies the instance variables of +object+ into +self+. - -Instance variable names in the +exclude+ array are ignored. If +object+ -responds to +protected_instance_variables+ the ones returned are -also ignored. For example, Rails controllers implement that method. - -In both arrays strings and symbols are understood, and they have to include -the at sign. - -<ruby> -class C - def initialize(x, y, z) - @x, @y, @z = x, y, z - end - - def protected_instance_variables - %w(@z) - end -end - -a = C.new(0, 1, 2) -b = C.new(3, 4, 5) - -a.copy_instance_variables_from(b, [:@y]) -# a is now: @x = 3, @y = 1, @z = 2 -</ruby> - -In the example +object+ and +self+ are of the same type, but they don't need to. - -NOTE: Defined in +active_support/core_ext/object/instance_variables.rb+. - h4. Silencing Warnings, Streams, and Exceptions The methods +silence_warnings+ and +enable_warnings+ change the value of +$VERBOSE+ accordingly for the duration of their block, and reset it afterwards: @@ -1216,6 +1189,12 @@ To insert something verbatim use the +raw+ helper rather than calling +html_safe <%= raw @cms.current_template %> <%# inserts @cms.current_template as is %> </erb> +or, equivalently, use <tt><%==</tt>: + +<erb> +<%== @cms.current_template %> <%# inserts @cms.current_template as is %> +</erb> + The +raw+ helper calls +html_safe+ for you: <ruby> @@ -3428,7 +3407,5 @@ NOTE: Defined in +active_support/core_ext/load_error.rb+. h3. Changelog -"Lighthouse ticket":https://rails.lighthouseapp.com/projects/16213/tickets/67 - * August 10, 2010: Starts to take shape, added to the index. * April 18, 2009: Initial version by "Xavier Noria":credits.html#fxn diff --git a/railties/guides/source/api_documentation_guidelines.textile b/railties/guides/source/api_documentation_guidelines.textile index 900b3fb5d5..e3ccd6396c 100644 --- a/railties/guides/source/api_documentation_guidelines.textile +++ b/railties/guides/source/api_documentation_guidelines.textile @@ -116,14 +116,12 @@ Use fixed-width fonts for: * file names <ruby> -# Copies the instance variables of +object+ into +self+. -# -# Instance variable names in the +exclude+ array are ignored. If +object+ -# responds to <tt>protected_instance_variables</tt> the ones returned are -# also ignored. For example, Rails controllers implement that method. -# ... -def copy_instance_variables_from(object, exclude = []) - ... +class Array + # Calls <tt>to_param</tt> on all its elements and joins the result with + # slashes. This is used by <tt>url_for</tt> in Action Pack. + def to_param + collect { |e| e.to_param }.join '/' + end end </ruby> diff --git a/railties/guides/source/association_basics.textile b/railties/guides/source/association_basics.textile index f6d61373e1..14bbe907f3 100644 --- a/railties/guides/source/association_basics.textile +++ b/railties/guides/source/association_basics.textile @@ -1876,8 +1876,6 @@ Extensions can refer to the internals of the association proxy using these three h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/11 - * April 7, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * April 19, 2009: Added +:touch+ option to +belongs_to+ associations by "Mike Gunderloy":credits.html#mgunderloy * February 1, 2009: Added +:autosave+ option "Mike Gunderloy":credits.html#mgunderloy diff --git a/railties/guides/source/caching_with_rails.textile b/railties/guides/source/caching_with_rails.textile index 3320b610e7..63c52da32a 100644 --- a/railties/guides/source/caching_with_rails.textile +++ b/railties/guides/source/caching_with_rails.textile @@ -368,7 +368,6 @@ h3. Further reading h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/10-guide-to-caching * May 02, 2009: Formatting cleanups * April 26, 2009: Clean up typos in submitted patch diff --git a/railties/guides/source/command_line.textile b/railties/guides/source/command_line.textile index 752b5926f7..acd105c622 100644 --- a/railties/guides/source/command_line.textile +++ b/railties/guides/source/command_line.textile @@ -590,6 +590,3 @@ h5. Miscellaneous Tasks +rake routes+ will list all of your defined routes, which is useful for tracking down routing problems in your app, or giving you a good overview of the URLs in an app you're trying to get familiar with. -h3. Changelog - -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/29 diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index bb38c64307..fcf7ba0ae5 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -13,7 +13,7 @@ Rails offers (at least) four good spots to place initialization code: * application.rb * Environment-specific Configuration Files -* Initializers (load_application_initializers) +* Initializers * After-Initializers h3. Running Code Before Rails @@ -23,39 +23,63 @@ To run some code before Rails itself is loaded, simply put it above the call to h3. Configuring Rails Components -In general, the work of configuring Rails means configuring the components of Rails, as well as configuring Rails itself. The +application.rb+ and environment-specific configuration files (such as +config/environments/production.rb+) allow you to specify the various settings that you want to pass down to all of the components. For example, the default Rails 2.3 +application.rb+ file includes one setting: +In general, the work of configuring Rails means configuring the components of Rails, as well as configuring Rails itself. The +application.rb+ and environment-specific configuration files (such as +config/environments/production.rb+) allow you to specify the various settings that you want to pass down to all of the components. For example, the default Rails 3.0 +application.rb+ file includes this setting: <ruby> -config.filter_parameters << :password + config.filter_parameters += [:password] </ruby> This is a setting for Rails itself. If you want to pass settings to individual Rails components, you can do so via the same +config+ object: <ruby> -config.active_record.colorize_logging = false + config.active_record.timestamped_migrations = false </ruby> Rails will use that particular setting to configure Active Record. h4. Rails General Configuration -* +config.routes_configuration_file+ overrides the default path for the routes configuration file. This defaults to +config/routes.rb+. +* +config.after_initialize+ takes a block which will be ran _after_ Rails has finished initializing. Useful for configuring values set up by other initializers: -* +config.cache_classes+ controls whether or not application classes should be reloaded on each request. +<ruby> + config.after_initialize do + ActionView::Base.sanitized_allowed_tags.delete 'div' + end +</ruby> + +* +config.allow_concurrency+ should be set to +true+ to allow concurrent (threadsafe) action processing. Set to +false+ by default. You probably don't want to call this one directly, though, because a series of other adjustments need to be made for threadsafe mode to work properly. Can also be enabled with +threadsafe!+. + +* +config.asset_host+ sets the host for the assets. Useful when CDNs are used for hosting assets rather than the application server itself. Shorter version of +config.action_controller.asset_host+. + +* +config.asset_path+ takes a block which configures where assets can be found. Shorter version of +config.action_controller.asset_path+. + +<ruby> + config.asset_path = proc { |asset_path| "assets/#{asset_path}" } +</ruby> + +* +config.autoload_once_paths+ accepts an array of paths from which Rails will automatically load from only once. All elements of this array must also be in +autoload_paths+. + +* +config.autoload_paths+ accepts an array of additional paths to prepend to the load path. By default, all app, lib, vendor and mock paths are included in this list. + +* +config.cache_classes+ controls whether or not application classes should be reloaded on each request. Defaults to _true_ in development, _false_ in test and production. Can also be enabled with +threadsafe!+. * +config.cache_store+ configures which cache store to use for Rails caching. Options include +:memory_store+, +:file_store+, +:mem_cache_store+ or the name of your own custom class. -* +config.controller_paths+ accepts an array of paths that will be searched for controllers. Defaults to +app/controllers+. +* +config.colorize_logging+ specifies whether or not to use ANSI color codes when logging information. Defaults to _true_. + +* +config.consider_all_requests_local+ is generally set to +true+ during development and +false+ during production; if it is set to +true+, then any error will cause detailed debugging information to be dumped in the HTTP response. For finer-grained control, set this to +false+ and implement +local_request?+ in controllers to specify which requests should provide debugging information on errors. -* +config.database_configuration_file+ overrides the default path for the database configuration file. Default to +config/database.yml+. +* +config.controller_paths+ configures where Rails can find controllers for this application. -* +config.dependency_loading+ enables or disables dependency loading during the request cycle. Setting dependency_loading to _true_ will allow new classes to be loaded during a request and setting it to _false_ will disable this behavior. +* +config.dependency_loading+ enables or disables dependency loading during the request cycle. Setting dependency_loading to _true_ will allow new classes to be loaded during a request and setting it to _false_ will disable this behavior. Can also be enabled with +threadsafe!+. * +config.eager_load_paths+ accepts an array of paths from which Rails will eager load on boot if cache classes is enabled. All elements of this array must also be in +load_paths+. -* +config.load_once_paths+ accepts an array of paths from which Rails will automatically load from only once. All elements of this array must also be in +load_paths+. +* +config.encoding+ sets up the application-wide encoding. Defaults to UTF-8. -* +config.load_paths+ accepts an array of additional paths to prepend to the load path. By default, all app, lib, vendor and mock paths are included in this list. +* +config.filter_parameters+ used for filtering out the parameters that you don't want shown in the logs, such as passwords or credit card numbers. + +* +config.helper_paths+ configures where Rails can find helpers for this application. * +config.log_level+ defines the verbosity of the Rails logger. In production mode, this defaults to +:info+. In development mode, it defaults to +:debug+. @@ -63,26 +87,99 @@ h4. Rails General Configuration * +config.logger+ accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then used to log information from Action Controller. Set to nil to disable logging. -* +config.plugin_loader+ overrides the class that handles loading each plugin. Defaults to +Rails::Plugin::Loader+. - -* +config.plugin_locators+ overrides the class that handle finding the desired plugins that you‘d like to load for your application. By default it is the +Rails::Plugin::FileSystemLocator+. - -* +config.plugin_paths+ overrides the path to the root of the plugins directory. Defaults to +vendor/plugins+. +* +config.middleware+ allows you to configure the application's middleware. This is covered in depth in the "Configuring Middleware" section below. * +config.plugins+ accepts the list of plugins to load. If this is set to nil, all plugins will be loaded. If this is set to [], no plugins will be loaded. Otherwise, plugins will be loaded in the order specified. -* +config.preload_frameworks+ enables or disables preloading all frameworks at startup. +* +config.preload_frameworks+ enables or disables preloading all frameworks at startup. Can also be enabled with +threadsafe!+. * +config.reload_plugins+ enables or disables plugin reloading. -* +config.root_path+ configures the root path of the application. +* +config.root+ configures the root path of the application. -* +config.time_zone+ sets the default time zone for the application and enables time zone awareness for Active Record. +* +config.secret_token+ used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. + +* +config.serve_static_assets+ configures Rails to serve static assets. Defaults to _true_, but in the production environment is turned off. The server software used to run the application should be used to serve the assets instead. + +* +config.threadsafe!+ enables +allow_concurrency+, +cache_classes+, +dependency_loading+ and +preload_frameworks+ to make the application threadsafe. -* +config.view_path+ sets the path of the root of an application's views. Defaults to +app/views+. +WARNING: Threadsafe operation is incompatible with the normal workings of development mode Rails. In particular, automatic dependency loading and class reloading are automatically disabled when you call +config.threadsafe!+. + +* +config.time_zone+ sets the default time zone for the application and enables time zone awareness for Active Record. * +config.whiny_nils+ enables or disabled warnings when an methods of nil are invoked. Defaults to _false_. +h4. Configuring Generators + +Rails 3 allows you to alter what generators are used with the +config.generators+ method. This method takes a block: + +<ruby> + config.generators do |g| + g.orm :active_record + g.test_framework :test_unit + end +</ruby> + +The full set of methods that can be used in this block are as follows: + +* +force_plural+ allows pluralized model names. Defaults to _false_. +* +helper+ defines whether or not to generate helpers. Defaults to _true_ +* +orm+ defines which orm to use. Defaults to _nil_, so will use Active Record by default. +* +integration_tool+ defines which integration tool to use. Defaults to _nil_ +* +performance_tool+ defines which performance tool to use. Defaults to _nil_ +* +resource_controller+ defines which generator to use for generating a controller when using +rails generate resource+. Defaults to +:controller+. +* +scaffold_controller+ different from +resource_controller+, defines which generator to use for generating a _scaffolded_ controller when using +rails generate scaffold+. Defaults to +:scaffold_controller+ +* +stylesheets+ turns on the hook for stylesheets in generators. Used in Rails for when the +scaffold+ generator is ran, but this hook can be used in other generates as well. +* +test_framework+ defines which test framework to use. Defaults to _nil_, so will use Test::Unit by default. +* +template_engine+ defines which template engine to use, such as ERB or Haml. Defaults to +:erb+. + +h4. Configuring Middleware + +Every Rails application comes with a standard set of middleware which it uses in this order in the development environment: + +* +ActionDispatch::Static+ is used to serve static assets. Disabled if +config.serve_static_assets+ is _true_. +* +Rack::Lock+ Will wrap the app in mutex so it can only be called by a single thread at a time. Only enabled if +config.action_controller.allow_concurrency+ is set to _false_, which it is by default. +* +ActiveSupport::Cache::Strategy::LocalCache+ Serves as a basic memory backed cache. This cache is not thread safe and is intended only for serving as a temporary memory cache for a single thread. +* +Rack::Runtime+ Sets an +X-Runtime+ header, containing the time (in seconds) taken to execute the request. +* +Rails::Rack::Logger+ Will notify the logs that the request has began. After request is complete, flushes all the logs. +* +ActionDispatch::ShowExceptions+ rescues any exception returned by the application and renders nice exception pages if the request is local or if +config.consider_all_requests_local+ is set to _true_. If +config.action_dispatch.show_exceptions+ is set to _false_, exceptions will be raised regardless. +* +ActionDispatch::RemoteIp+ checks for IP spoofing attacks. Configurable with the +config.action_dispatch.ip_spoofing_check+ and +config.action_dispatch.trusted_proxies+ settings. +* +Rack::Sendfile+ The Sendfile middleware intercepts responses whose body is being served from a file and replaces it with a server specific X-Sendfile header. Configurable with +config.action_dispatch_ +* +ActionDispatch::Callbacks+ Runs the prepare callbacks before serving the request. +* +ActiveRecord::ConnectionAdapters::ConnectionManagement+ cleans active connections after each request, unless the +rack.test+ key in the request environment is set to _true_. +* +ActiveRecord::QueryCache+ caches all +SELECT+ queries generated in a request. If an +INSERT+ or +UPDATE+ takes place then the cache is cleaned. +* +ActionDispatch::Cookies+ sets cookies for the request. +* +ActionDispatch::Session::CookieStore+ is responsible for storing the session in cookies. An alternate middleware can be used for this by changing the +config.action_controller.session_store+ to an alternate value. Additionally, options passed to this can be configured by using +config.action_controller.session_options+. +* +ActionDispatch::Flash+ sets up the +flash+ keys. Only available if +config.action_controller.session_store+ is set to a value. +* +ActionDispatch::ParamsParser+ parses out parameters from the request into +params+ +* +Rack::MethodOverride+ allows the method to be overridden if +params[:_method]+ is set. This is the middleware which supports the PUT and DELETE HTTP method types. +* +ActionDispatch::Head+ converts HEAD requests to GET requests and serves them as so. +* +ActionDispatch::BestStandardsSupport+ enables "best standards support" so that IE8 renders some elements correctly. + +Besides these usual middleware, you can add your own by using the +config.middleware.use+ method: + +<ruby> + config.middleware.use Magical::Unicorns +</ruby> + +This will put the +Magical::Unicorns+ middleware on the end of the stack. If you wish to put this middleware before another use +insert_before+: + +<ruby> + config.middleware.insert_before ActionDispatch::Head, Magical::Unicorns +</ruby> + +There's also +insert_after+ which will insert a middleware _after_ another: + +<ruby> + config.middleware.insert_after ActionDispatch::Head, Magical::Unicorns +</ruby> + +Middlewares can also be completely swapped out and replaced with others: + +<ruby> + config.middleware.swap ActionDispatch::BestStandardsSupport, Magical::Unicorns +</ruby> + h4. Configuring i18n * +config.i18n.default_locale+ sets the default locale of an application used for i18n. Defaults to +:en+. @@ -105,8 +202,6 @@ h4. Configuring Active Record * +config.active_record.pluralize_table_names+ specifies whether Rails will look for singular or plural table names in the database. If set to +true+ (the default), then the Customer class will use the +customers+ table. If set to +false+, then the Customers class will use the +customer+ table. -* +config.active_record.colorize_logging+ (true by default) specifies whether or not to use ANSI color codes when logging information from ActiveRecord. - * +config.active_record.default_timezone+ determines whether to use +Time.local+ (if set to +:local+) or +Time.utc+ (if set to +:utc+) when pulling dates and times from the database. The default is +:local+. * +config.active_record.schema_format+ controls the format for dumping the database schema to a file. The options are +:ruby+ (the default) for a database-independent version that depends on migrations, or +:sql+ for a set of (potentially database-dependent) SQL statements. @@ -127,34 +222,26 @@ h4. Configuring Action Controller <tt>config.action_controller</tt> includes a number of configuration settings: -* +config.action_controller.asset_host+ provides a string that is prepended to all of the URL-generating helpers in +AssetHelper+. This is designed to allow moving all javascript, CSS, and image files to a separate asset host. +* +config.action_controller.asset_host+ sets the host for the assets. Useful when CDNs are used for hosting assets rather than the application server itself. -* +config.action_controller.asset_path+ allows you to override the default asset path generation by providing your own instructions. +* +config.action_controller.asset_path+ takes a block which configures where assets can be found. Shorter version of +config.action_controller.asset_path+. -* +config.action_controller.consider_all_requests_local+ is generally set to +true+ during development and +false+ during production; if it is set to +true+, then any error will cause detailed debugging information to be dumped in the HTTP response. For finer-grained control, set this to +false+ and implement +local_request?+ to specify which requests should provide debugging information on errors. +* +config.action_controller.page_cache_directory+ should be the document root for the web server and is set using <tt>Base.page_cache_directory = "/document/root"</tt>. For Rails, this directory has already been set to Rails.public_path (which is usually set to <tt>Rails.root + "/public"</tt>). Changing this setting can be useful to avoid naming conflicts with files in <tt>public/</tt>, but doing so will likely require configuring your web server to look in the new location for cached files. -* +config.action_controller.allow_concurrency+ should be set to +true+ to allow concurrent (threadsafe) action processing. Set to +false+ by default. You probably don't want to call this one directly, though, because a series of other adjustments need to be made for threadsafe mode to work properly. Instead, you should simply call +config.threadsafe!+ inside your +production.rb+ file, which makes all the necessary adjustments. - -WARNING: Threadsafe operation is incompatible with the normal workings of development mode Rails. In particular, automatic dependency loading and class reloading are automatically disabled when you call +config.threadsafe!+. +* +config.action_controller.page_cache_extension+ configures the extension used for cached pages saved to +page_cache_directory+. Defaults to +.html+ -* +config.action_controller.param_parsers+ provides an array of handlers that can extract information from incoming HTTP requests and add it to the +params+ hash. By default, parsers for multipart forms, URL-encoded forms, XML, and JSON are active. +* +config.action_controller.perform_caching+ configures whether the application should perform caching or not. Set to _false_ in development mode, _true_ in production. * +config.action_controller.default_charset+ specifies the default character set for all renders. The default is "utf-8". * +config.action_controller.logger+ accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then used to log information from Action Controller. Set to nil to disable logging. -* +config.action_controller.resource_path_names+ is a hash of default names for several RESTful actions. By default, the new action is named +new+ and the edit action is named +edit+. - * +config.action_controller.request_forgery_protection_token+ sets the token parameter name for RequestForgery. Calling +protect_from_forgery+ sets it to +:authenticity_token+ by default. -* +config.action_controller.optimise_named_routes+ turns on some optimizations in generating the routing table. It is set to +true+ by default. - * +config.action_controller.allow_forgery_protection+ enables or disables CSRF protection. By default this is +false+ in test mode and +true+ in all other modes. * +config.action_controller.relative_url_root+ can be used to tell Rails that you are deploying to a subdirectory. The default is +ENV['RAILS_RELATIVE_URL_ROOT']+. -* +config.action_dispatch.session_store+ sets the name of the store for session data. The default is +:cookie_store+; other valid options include +:active_record_store+, +:mem_cache_store+ or the name of your own custom class. - The caching code adds two additional settings: * +ActionController::Base.page_cache_directory+ sets the directory where Rails will create cached pages for your web server. The default is +Rails.public_path+ (which is usually set to +Rails.root + "/public"+). @@ -169,14 +256,16 @@ The Active Record session store can also be configured: * +ActiveRecord::SessionStore::Session.data_column_name+ sets the name of the column which stores marshaled session data. Defaults to +data+. +h4. Configuring Action Dispatch + +* +config.action_dispatch.session_store+ sets the name of the store for session data. The default is +:cookie_store+; other valid options include +:active_record_store+, +:mem_cache_store+ or the name of your own custom class. + h4. Configuring Action View There are only a few configuration options for Action View, starting with four on +ActionView::Base+: * +config.action_view.debug_rjs+ specifies whether RJS responses should be wrapped in a try/catch block that alert()s the caught exception (and then re-raises it). The default is +false+. -* +config.action_view.warn_cache_misses+ tells Rails to display a warning whenever an action results in a cache miss on your view paths. The default is +false+. - * +config.action_view.field_error_proc+ provides an HTML generator for displaying errors that come from Active Record. The default is <tt>Proc.new{ |html_tag, instance| %Q(%<div class="field_with_errors">#{html_tag}</div>).html_safe }</tt> * +config.action_view.default_form_builder+ tells Rails which form builder to use by default. The default is +ActionView::Helpers::FormBuilder+. @@ -189,8 +278,6 @@ h4. Configuring Action Mailer There are a number of settings available on +config.action_mailer+: -* +config.action_mailer.template_root+ gives the root folder for Action Mailer templates. - * +config.action_mailer.logger+ accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then used to log information from Action Mailer. Set to nil to disable logging. * +config.action_mailer.smtp_settings+ allows detailed configuration for the +:smtp+ delivery method. It accepts a hash of options, which can include any of these options: @@ -211,15 +298,13 @@ There are a number of settings available on +config.action_mailer+: * +config.action_mailer.perform_deliveries+ specifies whether mail will actually be delivered. By default this is +true+; it can be convenient to set it to +false+ for testing. -* +config.action_mailer.default_charset+ tells Action Mailer which character set to use for the body and for encoding the subject. It defaults to +utf-8+. - -* +config.action_mailer.default_content_type+ specifies the default content type used for the main part of the message. It defaults to "text/plain" - -* +config.action_mailer.default_mime_version+ is the default MIME version for the message. It defaults to +1.0+. - -* +config.action_mailer.default_implicit_parts_order+ - When a message is built implicitly (i.e. multiple parts are assembled from templates -which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to +["text/html", "text/enriched", "text/plain"]+. Items that appear first in the array have higher priority in the mail client -and appear last in the mime encoded message. +* +config.action_mailer.default+ configures Action Mailer defaults. These default to: +<ruby> + :mime_version => "1.0", + :charset => "UTF-8", + :content_type => "text/plain", + :parts_order => [ "text/plain", "text/enriched", "text/html" ] +</ruby> h4. Configuring Active Resource @@ -241,25 +326,22 @@ There are a few configuration options available in Active Support: * +ActiveSupport::Logger.silencer+ is set to +false+ to disable the ability to silence logging in a block. The default is +true+. -h3. Using Initializers +h3. Initialization events -After loading the framework and any gems and plugins in your application, Rails turns to loading initializers. An initializer is any file of Ruby code stored under +config/initializers+ in your application. You can use initializers to hold configuration settings that should be made after all of the frameworks and plugins are loaded. - -NOTE: You can use subfolders to organize your initializers if you like, because Rails will look into the whole file hierarchy from the +initializers+ folder on down. +Rails has 4 initialization events which can be hooked into (listed in order that they are ran): -TIP: If you have any ordering dependency in your initializers, you can control the load order by naming. For example, +01_critical.rb+ will be loaded before +02_normal.rb+. +* +before_configuration+: This is run as soon as the application constant inherits from +Rails::Application+. The +config+ calls are evaluated before this happens. +* +before_eager_load+: This is run directly before eager loading occurs, which is the default behaviour for the _production_ environment and not for the +development+ enviroment. +* +before_initialize+ This is run directly before the initialization process of the application occurs. +* +after_initialize+ Run directly after the initialization of the application, but before the application initializers are run. -h3. Using an After-Initializer +WARNING: Some parts of your application, notably observers and routing, are not yet set up at the point where the +after_initialize+ block is called. -After-initializers are run (as you might guess) after any initializers are loaded. You can supply an +after_initialize+ block (or an array of such blocks) by setting up +config.after_initialize+ in any of the Rails configuration files: +After loading the framework and any gems and plugins in your application, Rails turns to loading initializers. An initializer is any file of Ruby code stored under +config/initializers+ in your application. You can use initializers to hold configuration settings that should be made after all of the frameworks and plugins are loaded. -<ruby> -config.after_initialize do - SomeClass.init -end -</ruby> +NOTE: You can use subfolders to organize your initializers if you like, because Rails will look into the whole file hierarchy from the +initializers+ folder on down. -WARNING: Some parts of your application, notably observers and routing, are not yet set up at the point where the +after_initialize+ block is called. +TIP: If you have any ordering dependency in your initializers, you can control the load order by naming. For example, +01_critical.rb+ will be loaded before +02_normal.rb+. h3. Rails Environment Settings @@ -273,12 +355,9 @@ Some parts of Rails can also be configured externally by supplying environment v * +ENV["RAILS_CACHE_ID"]+ and +ENV["RAILS_APP_VERSION"]+ are used to generate expanded cache keys in Rails' caching code. This allows you to have multiple separate caches from the same application. -* +ENV['RAILS_GEM_VERSION']+ defines the version of the Rails gems to use, if +RAILS_GEM_VERSION+ is not defined in your +environment.rb+ file. - h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/28 - +* November 26, 2010: Removed all config settings not available in Rails 3 (Ryan Bigg) * August 13, 2009: Updated with config syntax and added general configuration options by "John Pignata" * January 3, 2009: First reasonably complete draft by "Mike Gunderloy":credits.html#mgunderloy * November 5, 2008: Rough outline by "Mike Gunderloy":credits.html#mgunderloy diff --git a/railties/guides/source/contribute.textile b/railties/guides/source/contribute.textile index 88c5c79e9d..3d4607de1d 100644 --- a/railties/guides/source/contribute.textile +++ b/railties/guides/source/contribute.textile @@ -1,6 +1,6 @@ h2. Contribute to the Rails Guides -Rails Guides aim to improve the Rails documentation and to make the barrier to entry as low as possible. A reasonably experienced developer should be able to use the Guides to come up to speed on Rails quickly. You can track the overall effort at the "Rails Guides Lighthouse":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets. Our sponsors have contributed prizes for those who write an entire guide, but there are many other ways to contribute. +Rails Guides aim to improve the Rails documentation and to make the barrier to entry as low as possible. A reasonably experienced developer should be able to use the guides to come up to speed on Rails quickly. Our sponsors have contributed prizes for those who write an entire guide, but there are many other ways to contribute. endprologue. diff --git a/railties/guides/source/contributing_to_rails.textile b/railties/guides/source/contributing_to_rails.textile index 7184759610..1a1f4e9858 100644 --- a/railties/guides/source/contributing_to_rails.textile +++ b/railties/guides/source/contributing_to_rails.textile @@ -105,12 +105,17 @@ mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost'; </shell> -Then ensure you run bundle install without the +--without db+ option: +Now you'll have to install Active Record dependencies. This step is a little tricky because just running +bundle install+ without the +--without db+ parameter won't get those dependencies installed. It turns out that bundler remembers the +--without db+ parameter between calls so you'll have to manually override this. (See the "+bundle_install+ man page":http://gembundler.com/man/bundle-install.1.html for details) + +The easiest way to do this is to remove bundler's config file and then run +install+ again: <shell> +rm .bundle/config bundle install </shell> +INFO: If you don't feel comfortable deleting bundler's config file, you can achieve the same effect by manually removing the "+BUNDLE_WITHOUT: db+" line on +.bundle/config+. + Finally, enter this from the +activerecord+ directory to create the test databases: <shell> @@ -127,7 +132,7 @@ You can now run tests as you did for +sqlite3+: rake test_mysql </shell> -You can also +myqsl+ with +postgresql+, +jdbcmysql+, +jdbcsqlite3+ or +jdbcpostgresql+. Check out the file +activerecord/RUNNING_UNIT_TESTS+ for information on running more targeted database tests, or the file +ci/ci_build.rb+ to see the test suite that the Rails continuous integration server runs. +You can also replace +mysql+ with +postgresql+, +jdbcmysql+, +jdbcsqlite3+ or +jdbcpostgresql+. Check out the file +activerecord/RUNNING_UNIT_TESTS+ for information on running more targeted database tests, or the file +ci/ci_build.rb+ to see the test suite that the Rails continuous integration server runs. NOTE: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, and SQLite 3. Subtle differences between the various Active Record database adapters have been behind the rejection of many patches that looked OK when tested only against MySQL. @@ -189,7 +194,7 @@ h3. Contributing to the Rails Documentation Another area where you can help out if you're not yet ready to take the plunge to writing Rails core code is with Rails documentation. You can help with the Rails Guides or the Rails API documentation. -TIP: "docrails":http://github.com/lifo/docrails/tree/master is the documentation branch for Rails with an *open commit policy*. You can simply PM "lifo":http://github.com/lifo on Github and ask for the commit rights. Documentation changes made as part of the "docrails":http://github.com/lifo/docrails/tree/master project are merged back to the Rails master code from time to time. Check out the "original announcement":http://weblog.rubyonrails.org/2008/5/2/help-improve-rails-documentation-on-git-branch for more details. +TIP: "docrails":http://github.com/lifo/docrails/tree/master is the documentation branch for Rails with an *open commit policy*, it has public write access. Documentation changes made as part of the "docrails":http://github.com/lifo/docrails/tree/master project are merged back to the Rails master code from time to time. Check out the "original announcement":http://weblog.rubyonrails.org/2008/5/2/help-improve-rails-documentation-on-git-branch for more details. h4. The Rails Guides @@ -300,8 +305,6 @@ And then...think about your next contribution! h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/64 - * April 6, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * August 1, 2009: Updates/amplifications by "Mike Gunderloy":credits.html#mgunderloy * March 2, 2009: Initial draft by "Mike Gunderloy":credits.html#mgunderloy diff --git a/railties/guides/source/debugging_rails_applications.textile b/railties/guides/source/debugging_rails_applications.textile index 6eec18b8b9..6613fad406 100644 --- a/railties/guides/source/debugging_rails_applications.textile +++ b/railties/guides/source/debugging_rails_applications.textile @@ -127,8 +127,8 @@ Rails makes use of Ruby's standard +logger+ to write log information. You can al You can specify an alternative logger in your +environment.rb+ or any environment file: <ruby> -ActiveRecord::Base.logger = Logger.new(STDOUT) -ActiveRecord::Base.logger = Log4r::Logger.new("Application Log") +Rails.logger = Logger.new(STDOUT) +Rails.logger = Log4r::Logger.new("Application Log") </ruby> Or in the +Initializer+ section, add _any_ of the following @@ -142,13 +142,13 @@ TIP: By default, each log is created under +Rails.root/log/+ and the log file na h4. Log Levels -When something is logged it's printed into the corresponding log if the log level of the message is equal or higher than the configured log level. If you want to know the current log level you can call the +ActiveRecord::Base.logger.level+ method. +When something is logged it's printed into the corresponding log if the log level of the message is equal or higher than the configured log level. If you want to know the current log level you can call the +Rails.logger.level+ method. The available log levels are: +:debug+, +:info+, +:warn+, +:error+, and +:fatal+, corresponding to the log level numbers from 0 up to 4 respectively. To change the default log level, use <ruby> config.log_level = Logger::WARN # In any environment initializer, or -ActiveRecord::Base.logger.level = 0 # at any time +Rails.logger.level = 0 # at any time </ruby> This is useful when you want to log under development or staging, but you don't want to flood your production log with unnecessary information. @@ -704,8 +704,6 @@ h3. References h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/5 - * April 4, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * November 3, 2008: Accepted for publication. Added RJS, memory leaks and plugins chapters by "Emilio Tagua":credits.html#miloops * October 19, 2008: Copy editing pass by "Mike Gunderloy":credits.html#mgunderloy diff --git a/railties/guides/source/form_helpers.textile b/railties/guides/source/form_helpers.textile index 80e0421b48..e178a60307 100644 --- a/railties/guides/source/form_helpers.textile +++ b/railties/guides/source/form_helpers.textile @@ -67,7 +67,7 @@ A basic search form <% end %> </erb> -TIP: +search_path+ can be a named route specified in "routes.rb" as: <br /><code>match "search" => "search"</code>This declares for path "/search" to call action "search" from controller "search". +TIP: +search_path+ can be a named route specified in "routes.rb" as: <br /><code>match "search" => "search"</code> This declares that path "/search" will be handled by action "search" belonging to controller "search". The above view code will result in the following markup: @@ -107,7 +107,7 @@ WARNING: Do not delimit the second hash without doing so with the first hash, ot h4. Helpers for Generating Form Elements -Rails provides a series of helpers for generating form elements such as checkboxes, text fields, radio buttons, and so on. These basic helpers, with names ending in <notextile>_tag</notextile> such as +text_field_tag+, +check_box_tag+, etc., generate just a single +<input>+ element. The first parameter to these is always the name of the input. In the controller this name will be the key in the +params+ hash used to get the value entered by the user. For example, if the form contains +Rails provides a series of helpers for generating form elements such as checkboxes, text fields and radio buttons. These basic helpers, with names ending in <notextile>_tag</notextile> such as +text_field_tag+ and +check_box_tag+ generate just a single +<input>+ element. The first parameter to these is always the name of the input. In the controller this name will be the key in the +params+ hash used to get the value entered by the user. For example, if the form contains <erb> <%= text_field_tag(:query) %> @@ -127,18 +127,18 @@ Checkboxes are form controls that give the user a set of options they can enable <erb> <%= check_box_tag(:pet_dog) %> - <%= label_tag(:pet_dog, "I own a dog") %> +<%= label_tag(:pet_dog, "I own a dog") %> <%= check_box_tag(:pet_cat) %> - <%= label_tag(:pet_cat, "I own a cat") %> +<%= label_tag(:pet_cat, "I own a cat") %> </erb> output: <html> <input id="pet_dog" name="pet_dog" type="checkbox" value="1" /> - <label for="pet_dog">I own a dog</label> +<label for="pet_dog">I own a dog</label> <input id="pet_cat" name="pet_cat" type="checkbox" value="1" /> - <label for="pet_cat">I own a cat</label> +<label for="pet_cat">I own a cat</label> </html> The second parameter to +check_box_tag+ is the value of the input. This is the value that will be submitted by the browser if the checkbox is ticked (i.e. the value that will be present in the +params+ hash). With the above form you would check the value of +params[:pet_dog]+ and +params[:pet_cat]+ to see which pets the user owns. @@ -149,18 +149,18 @@ Radio buttons, while similar to checkboxes, are controls that specify a set of o <erb> <%= radio_button_tag(:age, "child") %> - <%= label_tag(:age_child, "I am younger than 21") %> +<%= label_tag(:age_child, "I am younger than 21") %> <%= radio_button_tag(:age, "adult") %> - <%= label_tag(:age_adult, "I'm over 21") %> +<%= label_tag(:age_adult, "I'm over 21") %> </erb> output: <html> <input id="age_child" name="age" type="radio" value="child" /> - <label for="age_child">I am younger than 21</label> +<label for="age_child">I am younger than 21</label> <input id="age_adult" name="age" type="radio" value="adult" /> - <label for="age_adult">I'm over 21</label> +<label for="age_adult">I'm over 21</label> </html> As with +check_box_tag+ the second parameter to +radio_button_tag+ is the value of the input. Because these two radio buttons share the same name (age) the user will only be able to select one and +params[:age]+ will contain either "child" or "adult". @@ -232,13 +232,13 @@ The corresponding view +app/views/articles/new.html.erb+ using +form_for+ looks <%= form_for @article, :url => { :action => "create" }, :html => {:class => "nifty_form"} do |f| %> <%= f.text_field :title %> <%= f.text_area :body, :size => "60x12" %> - <%= submit_tag "Create" %> + <%= f.submit "Create" %> <% end %> </erb> There are a few things to note here: -# +:article+ is the name of the model and +@article+ is the actual object being edited. +# +@article+ is the actual object being edited. # There is a single hash of options. Routing options are passed in the +:url+ hash, HTML options are passed in the +:html+ hash. # The +form_for+ method yields a *form builder* object (the +f+ variable). # Methods to create form controls are called *on* the form builder object +f+ @@ -284,7 +284,7 @@ h4. Relying on Record Identification The Article model is directly available to users of the application, so -- following the best practices for developing with Rails -- you should declare it *a resource*: <ruby> -map.resources :articles +resources :articles </ruby> TIP: Declaring a resource has a number of side-affects. See "Rails Routing From the Outside In":routing.html#resource-routing-the-rails-default for more information on setting up and using resources. @@ -294,13 +294,13 @@ When dealing with RESTful resources, calls to +form_for+ can get significantly e <ruby> ## Creating a new article # long-style: -form_for(:article, @article, :url => articles_path) +form_for(@article, :url => articles_path) # same thing, short-style (record identification gets used): form_for(@article) ## Editing an existing article # long-style: -form_for(:article, @article, :url => article_path(@article), :html => { :method => "put" }) +form_for(@article, :url => article_path(@article), :html => { :method => "put" }) # short-style: form_for(@article) </ruby> @@ -776,8 +776,6 @@ Many apps grow beyond simple forms editing a single object. For example when cre h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/1 - * April 6, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com h3. Authors diff --git a/railties/guides/source/generators.textile b/railties/guides/source/generators.textile index 0f2cbb76dc..99c34ed30f 100644 --- a/railties/guides/source/generators.textile +++ b/railties/guides/source/generators.textile @@ -92,7 +92,7 @@ class InitializerGenerator < Rails::Generators::NamedBase end </ruby> -First, notice that we are inheriting from +Rails::Generators::NamedBase+ instead of +Rails::Generators::Base+. This means that our generator expects at 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 at least one argument, which will be the name of the initializer, and will be available in our code in the variable +name+. We can see that by invoking the description of this new generator (don't forget to delete the old generator file): @@ -201,7 +201,7 @@ config.generators do |g| end </ruby> -If we generate another resource with the scaffold generator, 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 Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators. +If we generate another resource with the scaffold generator, we can see that neither stylesheets nor fixtures are created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators. To demonstrate this, we are going to create a new helper generator that simply adds some instance variable readers. First, we create a generator: @@ -363,8 +363,6 @@ Fallbacks allow your generators to have a single responsibility, increasing code h3. Changelog -"Lighthouse Ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/102 - * August 23, 2010: Edit pass by "Xavier Noria":credits.html#fxn * April 30, 2010: Reviewed by José Valim diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile index e592417dcb..902b7353c0 100644 --- a/railties/guides/source/getting_started.textile +++ b/railties/guides/source/getting_started.textile @@ -919,8 +919,6 @@ So first, we'll wire up the Post show template (+/app/views/posts/show.html.erb+ <h2>Add a comment:</h2> <%= form_for([@post, @post.comments.build]) do |f| %> - <%= f.error_messages %> - <div class="field"> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> @@ -989,8 +987,6 @@ Once we have made the new comment, we send the user back to the original post us <h2>Add a comment:</h2> <%= form_for([@post, @post.comments.build]) do |f| %> - <%= f.error_messages %> - <div class="field"> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> @@ -1057,8 +1053,6 @@ Then in the +app/views/posts/show.html.erb+ you can change it to look like the f <h2>Add a comment:</h2> <%= form_for([@post, @post.comments.build]) do |f| %> - <%= f.error_messages %> - <div class="field"> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> @@ -1086,8 +1080,6 @@ Lets also move that new comment section out to it's own partial, again, you crea <erb> <%= form_for([@post, @post.comments.build]) do |f| %> - <%= f.error_messages %> - <div class="field"> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> @@ -1477,8 +1469,6 @@ Two very common sources of data that are not UTF-8: h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/2 - * August 30, 2010: Minor editing after Rails 3 release by "Joost Baaij":http://www.spacebabies.nl * July 12, 2010: Fixes, editing and updating of code samples by "Jaime Iniesta":http://jaimeiniesta.com * May 16, 2010: Added a section on configuration gotchas to address common encoding problems that people might have by "Yehuda Katz":http://www.yehudakatz.com diff --git a/railties/guides/source/i18n.textile b/railties/guides/source/i18n.textile index 8a7e9fcae6..25c24ac7d7 100644 --- a/railties/guides/source/i18n.textile +++ b/railties/guides/source/i18n.textile @@ -127,7 +127,7 @@ If you want to translate your Rails application to a *single language other than However, you would probably like to *provide support for more locales* in your application. In such case, you need to set and pass the locale between requests. -WARNING: You may be tempted to store the chosen locale in a _session_ or a _cookie_. *Do not do so*. The locale should be transparent and a part of the URL. This way you don't break people's basic assumptions about the web itself: if you send a URL of some page to a friend, she should see the same page, same content. A fancy word for this would be that you're being "_RESTful_":http://en.wikipedia.org/wiki/Representational_State_Transfer. Read more about the RESTful approach in "Stefan Tilkov's articles":http://www.infoq.com/articles/rest-introduction. There may be some exceptions to this rule, which are discussed below. +WARNING: You may be tempted to store the chosen locale in a _session_ or a <em>cookie</em>. *Do not do so*. The locale should be transparent and a part of the URL. This way you don't break people's basic assumptions about the web itself: if you send a URL of some page to a friend, she should see the same page, same content. A fancy word for this would be that you're being "<em>RESTful</em>":http://en.wikipedia.org/wiki/Representational_State_Transfer. Read more about the RESTful approach in "Stefan Tilkov's articles":http://www.infoq.com/articles/rest-introduction. There may be some exceptions to this rule, which are discussed below. The _setting part_ is easy. You can set the locale in a +before_filter+ in the +ApplicationController+ like this: @@ -253,7 +253,7 @@ match '/:locale' => 'dashboard#index' Do take special care about the *order of your routes*, so this route declaration does not "eat" other ones. (You may want to add it directly before the +root :to+ declaration.) -IMPORTANT: This solution has currently one rather big *downside*. Due to the _default_url_options_ implementation, you have to pass the +:id+ option explicitly, like this: +link_to 'Show', book_url(:id => book)+ and not depend on Rails' magic in code like +link_to 'Show', book+. If this should be a problem, have a look at two plugins which simplify work with routes in this way: Sven Fuchs's "routing_filter":http://github.com/svenfuchs/routing-filter/tree/master and Raul Murciano's "translate_routes":http://github.com/raul/translate_routes/tree/master. See also the page "How to encode the current locale in the URL":http://rails-i18n.org/wiki/wikipages/how-to-encode-the-current-locale-in-the-url in the Rails i18n Wiki. +NOTE: Have a look at two plugins which simplify work with routes in this way: Sven Fuchs's "routing_filter":http://github.com/svenfuchs/routing-filter/tree/master and Raul Murciano's "translate_routes":http://github.com/raul/translate_routes/tree/master. h4. Setting the Locale from the Client Supplied Information @@ -278,7 +278,7 @@ def extract_locale_from_accept_language_header end </ruby> -Of course, in a production environment you would need much more robust code, and could use a plugin such as Iain Hecker's "http_accept_language":http://github.com/iain/http_accept_language/tree/master or even Rack middleware such as Ryan Tomayko's "locale":http://github.com/rtomayko/rack-contrib/blob/master/lib/rack/locale.rb. +Of course, in a production environment you would need much more robust code, and could use a plugin such as Iain Hecker's "http_accept_language":http://github.com/iain/http_accept_language/tree/master or even Rack middleware such as Ryan Tomayko's "locale":http://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb. h5. Using GeoIP (or Similar) Database @@ -464,24 +464,24 @@ I18n.t 'message' The +translate+ method also takes a +:scope+ option which can contain one or more additional keys that will be used to specify a “namespace” or scope for a translation key: <ruby> -I18n.t :invalid, :scope => [:activerecord, :errors, :messages] +I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages] </ruby> -This looks up the +:invalid+ message in the Active Record error messages. +This looks up the +:record_invalid+ message in the Active Record error messages. Additionally, both the key and scopes can be specified as dot-separated keys as in: <ruby> -I18n.translate :"activerecord.errors.messages.invalid" +I18n.translate "activerecord.errors.messages.record_invalid" </ruby> Thus the following calls are equivalent: <ruby> -I18n.t 'activerecord.errors.messages.invalid' -I18n.t 'errors.messages.invalid', :scope => :active_record -I18n.t :invalid, :scope => 'activerecord.errors.messages' -I18n.t :invalid, :scope => [:activerecord, :errors, :messages] +I18n.t 'activerecord.errors.messages.record_invalid' +I18n.t 'errors.messages.record_invalid', :scope => :active_record +I18n.t :record_invalid, :scope => 'activerecord.errors.messages' +I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages] </ruby> h5. Defaults @@ -672,11 +672,11 @@ Active Record validation error messages can also be translated easily. Active Re This gives you quite powerful means to flexibly adjust your messages to your application's needs. -Consider a User model with a +validates_presence_of+ validation for the name attribute like this: +Consider a User model with a validation for the name attribute like this: <ruby> class User < ActiveRecord::Base - validates_presence_of :name + validates :name, :presence => true end </ruby> @@ -697,7 +697,7 @@ activerecord.errors.models.user.attributes.name.blank activerecord.errors.models.user.blank activerecord.errors.messages.blank errors.attributes.name.blank -errors.messagges.blank +errors.messages.blank </ruby> When your models are additionally using inheritance then the messages are looked up in the inheritance chain. @@ -706,7 +706,7 @@ For example, you might have an Admin model inheriting from User: <ruby> class Admin < User - validates_presence_of :name + validates :name, :presence => true end </ruby> @@ -719,7 +719,7 @@ activerecord.errors.models.user.attributes.title.blank activerecord.errors.models.user.blank activerecord.errors.messages.blank errors.attributes.title.blank -errors.messagges.blank +errors.messages.blank </ruby> This way you can provide special translations for various error messages at different points in your models inheritance chain and in the attributes, models, or default scopes. @@ -733,27 +733,27 @@ So, for example, instead of the default error message +"can not be blank"+ you c * +count+, where available, can be used for pluralization if present: |_. validation |_.with option |_.message |_.interpolation| -| validates_confirmation_of | - | :confirmation | -| -| validates_acceptance_of | - | :accepted | -| -| validates_presence_of | - | :blank | -| -| validates_length_of | :within, :in | :too_short | count| -| validates_length_of | :within, :in | :too_long | count| -| validates_length_of | :is | :wrong_length | count| -| validates_length_of | :minimum | :too_short | count| -| validates_length_of | :maximum | :too_long | count| -| validates_uniqueness_of | - | :taken | -| -| validates_format_of | - | :invalid | -| -| validates_inclusion_of | - | :inclusion | -| -| validates_exclusion_of | - | :exclusion | -| -| validates_associated | - | :invalid | -| -| validates_numericality_of | - | :not_a_number | -| -| validates_numericality_of | :greater_than | :greater_than | count| -| validates_numericality_of | :greater_than_or_equal_to | :greater_than_or_equal_to | count| -| validates_numericality_of | :equal_to | :equal_to | count| -| validates_numericality_of | :less_than | :less_than | count| -| validates_numericality_of | :less_than_or_equal_to | :less_than_or_equal_to | count| -| validates_numericality_of | :odd | :odd | -| -| validates_numericality_of | :even | :even | -| +| confirmation | - | :confirmation | -| +| acceptance | - | :accepted | -| +| presence | - | :blank | -| +| length | :within, :in | :too_short | count| +| length | :within, :in | :too_long | count| +| length | :is | :wrong_length | count| +| length | :minimum | :too_short | count| +| length | :maximum | :too_long | count| +| uniqueness | - | :taken | -| +| format | - | :invalid | -| +| inclusion | - | :inclusion | -| +| exclusion | - | :exclusion | -| +| associated | - | :invalid | -| +| numericality | - | :not_a_number | -| +| numericality | :greater_than | :greater_than | count| +| numericality | :greater_than_or_equal_to | :greater_than_or_equal_to | count| +| numericality | :equal_to | :equal_to | count| +| numericality | :less_than | :less_than | count| +| numericality | :less_than_or_equal_to | :less_than_or_equal_to | count| +| numericality | :odd | :odd | -| +| numericality | :even | :even | -| h5. Translations for the Active Record +error_messages_for+ Helper @@ -889,8 +889,3 @@ fn1. Or, to quote "Wikipedia":http://en.wikipedia.org/wiki/Internationalization_ fn2. Other backends might allow or require to use other formats, e.g. a GetText backend might allow to read GetText files. fn3. One of these reasons is that we don't want to imply any unnecessary load for applications that do not need any I18n capabilities, so we need to keep the I18n library as simple as possible for English. Another reason is that it is virtually impossible to implement a one-fits-all solution for all problems related to I18n for all existing languages. So a solution that allows us to exchange the entire implementation easily is appropriate anyway. This also makes it much easier to experiment with custom features and extensions. - - -h3. Changelog - -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/23 diff --git a/railties/guides/source/index.html.erb b/railties/guides/source/index.html.erb index 6b897e3a6a..84fbc53a69 100644 --- a/railties/guides/source/index.html.erb +++ b/railties/guides/source/index.html.erb @@ -31,7 +31,7 @@ Ruby on Rails Guides <div id="subCol"> <dl> <dd class="warning">Rails Guides are a result of the ongoing <a href="http://hackfest.rubyonrails.org">Guides hackfest</a>, and a work in progress.</dd> - <dd class="ticket">Guides marked with this icon are currently being worked on. While they might still be useful to you, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections at the respective Lighthouse ticket.</dd> + <dd class="work-in-progress">Guides marked with this icon are currently being worked on. While they might still be useful to you, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections to the author.</dd> </dl> </div> <% end %> @@ -71,7 +71,7 @@ Ruby on Rails Guides <p>This guide covers the basic layout features of Action Controller and Action View, including rendering and redirecting, using content_for blocks, and working with partials.</p> <% end %> -<%= guide("Action View Form Helpers", 'form_helpers.html', :ticket => 1) do %> +<%= guide("Action View Form Helpers", 'form_helpers.html', :work_in_progress => true) do %> <p>Guide to using built-in Form helpers.</p> <% end %> </dl> @@ -100,11 +100,11 @@ Ruby on Rails Guides <p>This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country and so on.</p> <% end %> -<%= guide("Action Mailer Basics", 'action_mailer_basics.html', :ticket => 25) do %> +<%= guide("Action Mailer Basics", 'action_mailer_basics.html', :work_in_progress => true) do %> <p>This guide describes how to use Action Mailer to send and receive emails.</p> <% end %> -<%= guide("Testing Rails Applications", 'testing.html', :ticket => 8) do %> +<%= guide("Testing Rails Applications", 'testing.html', :work_in_progress => true) do %> <p>This is a rather comprehensive guide to doing both unit and functional tests in Rails. It covers everything from "What is a test?" to the testing APIs. Enjoy.</p> <% end %> @@ -124,11 +124,11 @@ Ruby on Rails Guides <p>This guide covers the basic configuration settings for a Rails application.</p> <% end %> -<%= guide("Rails Command Line Tools and Rake tasks", 'command_line.html', :ticket => 29) do %> +<%= guide("Rails Command Line Tools and Rake tasks", 'command_line.html', :work_in_progress => true) do %> <p>This guide covers the command line tools and rake tasks provided by Rails.</p> <% end %> -<%= guide("Caching with Rails", 'caching_with_rails.html', :ticket => 10) do %> +<%= guide("Caching with Rails", 'caching_with_rails.html', :work_in_progress => true) do %> <p>Various caching techniques provided by Rails.</p> <% end %> </dl> @@ -136,7 +136,7 @@ Ruby on Rails Guides <h3>Extending Rails</h3> <dl> - <%= guide("The Basics of Creating Rails Plugins", 'plugins.html', :ticket => 32) do %> + <%= guide("The Basics of Creating Rails Plugins", 'plugins.html', :work_in_progress => true) do %> <p>This guide covers how to build a plugin to extend the functionality of Rails.</p> <% end %> diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index 77c20c8bb0..4cc5f3843f 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -1479,10 +1479,10 @@ Next, the Railtie itself is defined: require "action_view/railties/log_subscriber" log_subscriber ActionView::Railties::LogSubscriber.new - initializer "action_view.cache_asset_timestamps" do |app| + initializer "action_view.cache_asset_id" do |app| unless app.config.cache_classes - ActionView.base_hook do - ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false + ActiveSupport.on_load(:action_view) do + ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false end end end @@ -1492,7 +1492,7 @@ Next, the Railtie itself is defined: The +ActionView::LogSubscriber+ sets up a method called +render_template+ which is called when a template is rendered. TODO: Templates only or partials and layouts also? I would imagine these fall under the templates category, but there needs to research to ensure this is correct. -The sole initializer defined here, _action_view.cache_asset_timestamps_ is responsible for caching the timestamps on the ends of your assets. If you've ever seen a link generated by +image_tag+ or +stylesheet_link_tag+ you would know that I mean that this timestamp is the number after the _?_ in this example: _/javascripts/prototype.js?1265442620_. This initializer will do nothing if +cache_classes+ is set to false in any of your application's configuration. TODO: Elaborate. +The sole initializer defined here, _action_view.cache_asset_ids_ is responsible for caching the timestamps on the ends of your assets. If you've ever seen a link generated by +image_tag+ or +stylesheet_link_tag+ you would know that I mean that this timestamp is the number after the _?_ in this example: _/javascripts/prototype.js?1265442620_. This initializer will do nothing if +cache_classes+ is set to false in any of your application's configuration. TODO: Elaborate. h4. Action Mailer Railtie @@ -3003,7 +3003,7 @@ The +I18n::Railtie+ also defines an +after_initialize+ which we will return to l **Action View Initializers ** -* action_view.cache_asset_timestamps +* action_view.cache_asset_ids **Action Mailer Initializers ** diff --git a/railties/guides/source/layout.html.erb b/railties/guides/source/layout.html.erb index d4b87dc45b..bb62506f04 100644 --- a/railties/guides/source/layout.html.erb +++ b/railties/guides/source/layout.html.erb @@ -106,6 +106,31 @@ <div class="wrapper"> <div id="mainCol"> <%= yield.html_safe %> + + <h3>Feedback</h3> + <p> + You're encouraged to help in keeping the quality of this guide. + </p> + <p> + If you see any typos or factual errors you are confident to + patch please clone <%= link_to 'docrails', 'https://github.com/lifo/docrails' %> + and push the change yourself. That branch of Rails has public write access. + Commits are still reviewed, but that happens after you've submitted your + contribution. <%= link_to 'docrails', 'https://github.com/lifo/docrails' %> is + cross-merged with master periodically. + </p> + <p> + You may also find incomplete content, or stuff that is not up to date. + Please do add any missing documentation for master. Check the + <%= link_to 'Ruby on Rails Guides Guidelines', 'ruby_on_rails_guides_guidelines.html' %> + guide for style and conventions. + </p> + <p> + Issues may also be reported <%= link_to 'in Github', 'https://github.com/lifo/docrails/issues' %>. + </p> + <p>And last but not least, any kind of discussion regarding Ruby on Rails + documentation is very welcome in the <%= link_to 'rubyonrails-docs mailing list', 'http://groups.google.com/group/rubyonrails-docs' %>. + </p> </div> </div> </div> diff --git a/railties/guides/source/layouts_and_rendering.textile b/railties/guides/source/layouts_and_rendering.textile index c65ea5c797..80a1fdd38d 100644 --- a/railties/guides/source/layouts_and_rendering.textile +++ b/railties/guides/source/layouts_and_rendering.textile @@ -970,7 +970,7 @@ Partial templates - usually just called "partials" - are another device for brea h5. Naming Partials -To render a partial as part of a view, you use the +render+ method within the view, and include the +:partial+ option: +To render a partial as part of a view, you use the +render+ method within the view: <ruby> <%= render "menu" %> @@ -1194,8 +1194,6 @@ There are several ways of getting similar results with different sub-templating h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/15 - * April 4, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * January 25, 2010: Rails 3.0 Update by "Mikel Lindsaar":credits.html#raasdnil * December 27, 2008: Merge patch from Rodrigo Rosenfeld Rosas covering subtemplates diff --git a/railties/guides/source/migrations.textile b/railties/guides/source/migrations.textile index 89aa007279..0d13fbc10a 100644 --- a/railties/guides/source/migrations.textile +++ b/railties/guides/source/migrations.textile @@ -546,7 +546,7 @@ Schema files are also useful if you want a quick look at what attributes an Acti h4. Types of Schema Dumps -There are two ways to dump the schema. This is set in +config/environment.rb+ by the +config.active_record.schema_format+ setting, which may be either +:sql+ or +:ruby+. +There are two ways to dump the schema. This is set in +config/application.rb+ by the +config.active_record.schema_format+ setting, which may be either +:sql+ or +:ruby+. If +:ruby+ is selected then the schema is stored in +db/schema.rb+. If you look at this file you'll find that it looks an awful lot like one very big migration: @@ -584,13 +584,11 @@ h3. Active Record and Referential Integrity The Active Record way claims that intelligence belongs in your models, not in the database. As such, features such as triggers or foreign key constraints, which push some of that intelligence back into the database, are not heavily used. -Validations such as +validates_uniqueness_of+ are one way in which models can enforce data integrity. The +:dependent+ option on associations allows models to automatically destroy child objects when the parent is destroyed. Like anything which operates at the application level these cannot guarantee referential integrity and so some people augment them with foreign key constraints. +Validations such as +validates :foreign_key, :uniqueness => true+ are one way in which models can enforce data integrity. The +:dependent+ option on associations allows models to automatically destroy child objects when the parent is destroyed. Like anything which operates at the application level these cannot guarantee referential integrity and so some people augment them with foreign key constraints. Although Active Record does not provide any tools for working directly with such features, the +execute+ method can be used to execute arbitrary SQL. There are also a number of plugins such as "foreign_key_migrations":http://github.com/harukizaemon/redhillonrails/tree/master/foreign_key_migrations/ which add foreign key support to Active Record (including support for dumping foreign keys in +db/schema.rb+). h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/6 - * July 15, 2010: minor typos corrected by "Jaime Iniesta":http://jaimeiniesta.com * September 14, 2008: initial version by "Frederick Cheung":credits.html#fcheung diff --git a/railties/guides/source/performance_testing.textile b/railties/guides/source/performance_testing.textile index 9dda6d420a..41bdd27e9b 100644 --- a/railties/guides/source/performance_testing.textile +++ b/railties/guides/source/performance_testing.textile @@ -524,7 +524,5 @@ Rails has been lucky to have two startups dedicated to Rails specific performanc h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/4 - * January 9, 2009: Complete rewrite by "Pratik":credits.html#lifo * September 6, 2008: Initial version by Matthew Bergman diff --git a/railties/guides/source/plugins.textile b/railties/guides/source/plugins.textile index 7c2725e652..cb43282ace 100644 --- a/railties/guides/source/plugins.textile +++ b/railties/guides/source/plugins.textile @@ -802,7 +802,7 @@ You can also see if your routes work by running +rake routes+ from your app dire h3. Generators -Many plugins ship with generators. When you created the plugin above, you specified the +--with-generator+ option, so you already have the generator stubs in 'vendor/plugins/yaffle/generators/yaffle'. +Many plugins ship with generators. When you created the plugin above, you specified the +--generator+ option, so you already have the generator stubs in 'vendor/plugins/yaffle/generators/yaffle'. Building generators is a complex topic unto itself and this section will cover one small aspect of generators: generating a simple text file. @@ -1513,7 +1513,5 @@ The final plugin should have a directory structure that looks something like thi h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/32-update-plugins-guide - * April 4, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * November 17, 2008: Major revision by Jeff Dean diff --git a/railties/guides/source/rails_application_templates.textile b/railties/guides/source/rails_application_templates.textile index bc7b151dfe..d4b887ad02 100644 --- a/railties/guides/source/rails_application_templates.textile +++ b/railties/guides/source/rails_application_templates.textile @@ -233,6 +233,4 @@ git :commit => "-a -m 'Initial commit'" h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/78 - * April 29, 2009: Initial version by "Pratik":credits.html#lifo diff --git a/railties/guides/source/rails_on_rack.textile b/railties/guides/source/rails_on_rack.textile index eaebb05f17..f17e9b4798 100644 --- a/railties/guides/source/rails_on_rack.textile +++ b/railties/guides/source/rails_on_rack.textile @@ -237,7 +237,5 @@ h4. Understanding Middlewares h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/58 - * February 7, 2009: Second version by "Pratik":credits.html#lifo * January 11, 2009: First version by "Pratik":credits.html#lifo diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index f48ae9c7f7..2f5c88b8c3 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -194,7 +194,7 @@ end or, for a single case <ruby> -resources :posts, :path => "/admin" +resources :posts, :path => "/admin/posts" </ruby> In each of these cases, the named routes remain the same as if you did not use +scope+. In the last case, the following paths map to +PostsController+: @@ -436,6 +436,26 @@ match 'exit' => 'sessions#destroy', :as => :logout This will create +logout_path+ and +logout_url+ as named helpers in your application. Calling +logout_path+ will return +/exit+ +h4. HTTP Verb Constraints + +You can use the +:via+ option to constrain the request to one or more HTTP methods: + +<ruby> +match 'photos/show' => 'photos#show', :via => :get +</ruby> + +There is a shorthand version of this as well: + +<ruby> +get 'photos/show' +</ruby> + +You can also permit more than one verb to a single route: + +<ruby> +match 'photos/show' => 'photos#show', :via => [:get, :post] +</ruby> + h4. Segment Constraints You can use the +:constraints+ option to enforce a format for a dynamic segment: @@ -833,8 +853,6 @@ assert_routing({ :path => "photos", :method => :post }, { :controller => "photos h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/3 - * April 10, 2010: Updated guide to remove outdated and superfluous information, and to provide information about new features, by "Yehuda Katz":http://www.yehudakatz.com * April 2, 2010: Updated guide to match new Routing DSL in Rails 3, by "Rizwan Reza":http://www.rizwanreza.com/ * Febuary 1, 2010: Modifies the routing documentation to match new routing DSL in Rails 3, by Prem Sichanugrist diff --git a/railties/guides/source/security.textile b/railties/guides/source/security.textile index 4656cf4e40..5b24d8c8e3 100644 --- a/railties/guides/source/security.textile +++ b/railties/guides/source/security.textile @@ -550,7 +550,7 @@ Ruby uses a slightly different approach than many other languages to match the e <ruby> class File < ActiveRecord::Base - validates_format_of :name, :with => /^[\w\.\-\+]+$/ + validates :name, :format => /^[\w\.\-\+]+$/ end </ruby> @@ -979,6 +979,4 @@ The security landscape shifts and it is important to keep up to date, because mi h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/7 - * November 1, 2008: First approved version by Heiko Webers diff --git a/railties/guides/source/testing.textile b/railties/guides/source/testing.textile index 043a2462b4..c292a5d83b 100644 --- a/railties/guides/source/testing.textile +++ b/railties/guides/source/testing.textile @@ -315,7 +315,7 @@ Now to get this test to pass we can add a model level validation for the _title_ <ruby> class Post < ActiveRecord::Base - validates_presence_of :title + validates :title, :presence => true end </ruby> @@ -940,8 +940,6 @@ The built-in +test/unit+ based testing is not the only way to test Rails applica h3. Changelog -"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/8 - * April 4, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * November 13, 2008: Revised based on feedback from Pratik Naik by "Akshay Surve":credits.html#asurve (not yet approved for publication) * October 14, 2008: Edit and formatting pass by "Mike Gunderloy":credits.html#mgunderloy (not yet approved for publication) diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index d84373cddf..182068071d 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -35,7 +35,6 @@ module Rails # class Application < Engine autoload :Bootstrap, 'rails/application/bootstrap' - autoload :Configurable, 'rails/application/configurable' autoload :Configuration, 'rails/application/configuration' autoload :Finisher, 'rails/application/finisher' autoload :Railties, 'rails/application/railties' diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb index 1260772605..2b32f7edf1 100644 --- a/railties/lib/rails/cli.rb +++ b/railties/lib/rails/cli.rb @@ -5,10 +5,12 @@ require 'rails/script_rails_loader' # the rest of this script is not run. Rails::ScriptRailsLoader.exec_script_rails! -railties_path = File.expand_path('../../lib', __FILE__) -$:.unshift(railties_path) if File.directory?(railties_path) && !$:.include?(railties_path) - require 'rails/ruby_version_check' Signal.trap("INT") { puts; exit } -require 'rails/commands/application' +if ARGV.first == 'plugin' + ARGV.shift + require 'rails/commands/plugin_new' +else + require 'rails/commands/application' +end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index e3aa6d7c3e..338565247f 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -12,14 +12,19 @@ command = aliases[command] || command case command when 'generate', 'destroy', 'plugin' - require APP_PATH - Rails.application.require_environment! + if command == 'plugin' && ARGV.first == 'new' + require "rails/commands/plugin_new" + else + require APP_PATH + Rails.application.require_environment! - if defined?(ENGINE_PATH) - engine = Rails.application.railties.engines.find { |r| r.root.to_s == ENGINE_PATH } - Rails.application = engine + if defined?(ENGINE_PATH) + engine = Rails.application.railties.engines.find { |r| r.root.to_s == ENGINE_PATH } + Rails.application = engine + end + + require "rails/commands/#{command}" end - require "rails/commands/#{command}" when 'benchmarker', 'profiler' require APP_PATH diff --git a/railties/lib/rails/commands/plugin_new.rb b/railties/lib/rails/commands/plugin_new.rb new file mode 100644 index 0000000000..8baa8ebfd4 --- /dev/null +++ b/railties/lib/rails/commands/plugin_new.rb @@ -0,0 +1,10 @@ +if ARGV.first != "new" + ARGV[0] = "--help" +else + ARGV.shift +end + +require 'rails/generators' +require 'rails/generators/rails/plugin_new/plugin_new_generator' + +Rails::Generators::PluginNewGenerator.start diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index 8369795e71..66fab0a760 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -1,5 +1,6 @@ require 'active_support/deprecation' require 'active_support/ordered_options' +require 'active_support/core_ext/hash/deep_dup' require 'rails/paths' require 'rails/rack' @@ -51,6 +52,13 @@ module Rails @colorize_logging = true end + def initialize_copy(source) + @aliases = @aliases.deep_dup + @options = @options.deep_dup + @fallbacks = @fallbacks.deep_dup + @templates = @templates.dup + end + def method_missing(method, *args) method = method.to_s.sub(/=$/, '').to_sym diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 72bfe207d7..85fa4424c4 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -324,11 +324,12 @@ module Rails # MyEngine::Engine.load_seed # class Engine < Railtie - autoload :Configurable, "rails/engine/configurable" autoload :Configuration, "rails/engine/configuration" + autoload :Railties, "rails/engine/railties" class << self attr_accessor :called_from, :isolated + alias :isolated? :isolated alias :engine_name :railtie_name def inherited(base) @@ -362,16 +363,14 @@ module Rails _railtie end - define_method(:table_name_prefix) do - "#{name}_" + unless mod.respond_to?(:table_name_prefix) + define_method(:table_name_prefix) do + "#{name}_" + end end end end end - - def isolated? - !!isolated - end end delegate :middleware, :root, :paths, :to => :config diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 7a07dcad7d..4f458b0aee 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -10,6 +10,7 @@ module Rails def initialize(root=nil) super() @root = root + @generators = app_generators.dup end # Returns the middleware stack for the engine. diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 240810b8bd..66c4088a68 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -228,6 +228,7 @@ module Rails rails = groups.delete("rails") rails.map! { |n| n.sub(/^rails:/, '') } rails.delete("app") + rails.delete("plugin_new") print_list("rails", rails) hidden_namespaces.each {|n| groups.delete(n.to_s) } @@ -301,6 +302,7 @@ module Rails $LOAD_PATH.each do |base| Dir[File.join(base, "{rails/generators,generators}", "**", "*_generator.rb")].each do |path| begin + path = path.sub("#{base}/", "") require path rescue Exception => e # No problem diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb new file mode 100644 index 0000000000..f5c626553c --- /dev/null +++ b/railties/lib/rails/generators/app_base.rb @@ -0,0 +1,176 @@ +require 'digest/md5' +require 'active_support/secure_random' +require 'rails/version' unless defined?(Rails::VERSION) +require 'rbconfig' +require 'open-uri' +require 'uri' + +module Rails + module Generators + class AppBase < Base + DATABASES = %w( mysql oracle postgresql sqlite3 frontbase ibm_db ) + JAVASCRIPTS = %w( prototype jquery ) + + attr_accessor :rails_template + add_shebang_option! + + argument :app_path, :type => :string + + def self.add_shared_options_for(name) + class_option :builder, :type => :string, :aliases => "-b", + :desc => "Path to a #{name} builder (can be a filesystem path or URL)" + + class_option :template, :type => :string, :aliases => "-m", + :desc => "Path to an #{name} template (can be a filesystem path or URL)" + + class_option :skip_gemfile, :type => :boolean, :default => false, + :desc => "Don't create a Gemfile" + + class_option :skip_git, :type => :boolean, :aliases => "-G", :default => false, + :desc => "Skip Git ignores and keeps" + + class_option :skip_active_record, :type => :boolean, :aliases => "-O", :default => false, + :desc => "Skip Active Record files" + + class_option :database, :type => :string, :aliases => "-d", :default => "sqlite3", + :desc => "Preconfigure for selected database (options: #{DATABASES.join('/')})" + + class_option :javascript, :type => :string, :aliases => "-j", :default => "prototype", + :desc => "Preconfigure for selected javascript library (options: #{JAVASCRIPTS.join('/')})" + + class_option :skip_javascript, :type => :boolean, :aliases => "-J", :default => false, + :desc => "Skip javascript files" + + class_option :dev, :type => :boolean, :default => false, + :desc => "Setup the #{name} with Gemfile pointing to your Rails checkout" + + class_option :edge, :type => :boolean, :default => false, + :desc => "Setup the #{name} with Gemfile pointing to Rails repository" + + class_option :skip_test_unit, :type => :boolean, :aliases => "-T", :default => false, + :desc => "Skip Test::Unit files" + + class_option :help, :type => :boolean, :aliases => "-h", :group => :rails, + :desc => "Show this help message and quit" + end + + def initialize(*args) + @original_wd = Dir.pwd + + super + end + + protected + + def builder + @builder ||= begin + if path = options[:builder] + if URI(path).is_a?(URI::HTTP) + contents = open(path, "Accept" => "application/x-thor-template") {|io| io.read } + else + contents = open(File.expand_path(path, @original_wd)) {|io| io.read } + end + + prok = eval("proc { #{contents} }", TOPLEVEL_BINDING, path, 1) + instance_eval(&prok) + end + + builder_class = get_builder_class + builder_class.send(:include, ActionMethods) + builder_class.new(self) + end + end + + def build(meth, *args) + builder.send(meth, *args) if builder.respond_to?(meth) + end + + def create_root + self.destination_root = File.expand_path(app_path, destination_root) + valid_const? + + empty_directory '.' + set_default_accessors! + FileUtils.cd(destination_root) unless options[:pretend] + end + + def apply_rails_template + apply rails_template if rails_template + rescue Thor::Error, LoadError, Errno::ENOENT => e + raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}" + end + + def set_default_accessors! + self.rails_template = case options[:template] + when /^http:\/\// + options[:template] + when String + File.expand_path(options[:template], Dir.pwd) + else + options[:template] + end + end + + def database_gemfile_entry + entry = "" + unless options[:skip_active_record] + entry = "gem '#{gem_for_database}'" + entry << ", :require => '#{require_for_database}'" if require_for_database + end + entry + end + + def rails_gemfile_entry + if options.dev? + <<-GEMFILE +gem 'rails', :path => '#{Rails::Generators::RAILS_DEV_PATH}' +gem 'arel', :git => 'git://github.com/rails/arel.git' +gem "rack", :git => "git://github.com/rack/rack.git" + GEMFILE + elsif options.edge? + <<-GEMFILE +gem 'rails', :git => 'git://github.com/rails/rails.git' +gem 'arel', :git => 'git://github.com/rails/arel.git' +gem "rack", :git => "git://github.com/rack/rack.git" + GEMFILE + else + <<-GEMFILE +gem 'rails', '#{Rails::VERSION::STRING}' + +# Bundle edge Rails instead: +# gem 'rails', :git => 'git://github.com/rails/rails.git' +# gem 'arel', :git => 'git://github.com/rails/arel.git' +# gem "rack", :git => "git://github.com/rack/rack.git" + GEMFILE + end + end + + def gem_for_database + # %w( mysql oracle postgresql sqlite3 frontbase ibm_db ) + case options[:database] + when "oracle" then "ruby-oci8" + when "postgresql" then "pg" + when "sqlite3" then "sqlite3-ruby" + when "frontbase" then "ruby-frontbase" + when "mysql" then "mysql2" + else options[:database] + end + end + + def require_for_database + case options[:database] + when "sqlite3" then "sqlite3" + end + end + + def bundle_if_dev_or_edge + bundle_command = File.basename(Thor::Util.ruby_command).sub(/ruby/, 'bundle') + run "#{bundle_command} install" if dev_or_edge? + end + + def dev_or_edge? + options.dev? || options.edge? + end + end + end +end diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 9131a19043..e0dde4360f 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -16,6 +16,14 @@ module Rails parse_attributes! if respond_to?(:attributes) end + no_tasks do + def template(source, *args, &block) + inside_template do + super + end + end + end + protected attr_reader :file_name alias :singular_name :file_name @@ -23,16 +31,14 @@ module Rails # Wrap block with namespace of current application # if namespace exists and is not skipped def module_namespacing(&block) - inside_namespace do - content = capture(&block) - content = wrap_with_namespace(content) if namespaced? - concat(content) - end + content = capture(&block) + content = wrap_with_namespace(content) if namespaced? + concat(content) end def indent(content, multiplier = 2) spaces = " " * multiplier - content.each_line.map {|line| "#{spaces}#{line}" }.join("\n") + content = content.each_line.map {|line| "#{spaces}#{line}" }.join end def wrap_with_namespace(content) @@ -40,12 +46,15 @@ module Rails "module #{namespace.name}\n#{content}\nend\n" end - def inside_namespace - @inside_namespace = true if namespaced? - result = yield - result + def inside_template + @inside_template = true + yield ensure - @inside_namespace = false + @inside_template = false + end + + def inside_template? + @inside_template end def namespace @@ -55,11 +64,7 @@ module Rails end def namespaced? - !options[:skip_namespace] && !!namespace - end - - def inside_namespace? - @inside_namespace + !options[:skip_namespace] && namespace end def file_path @@ -67,7 +72,7 @@ module Rails end def class_path - inside_namespace? || !namespaced? ? regular_class_path : namespaced_class_path + inside_template? || !namespaced? ? regular_class_path : namespaced_class_path end def regular_class_path diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 44f9fde0a6..ef1eb8d237 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -1,9 +1,4 @@ -require 'digest/md5' -require 'active_support/secure_random' -require 'rails/version' unless defined?(Rails::VERSION) -require 'rbconfig' -require 'open-uri' -require 'uri' +require 'rails/generators/app_base' module Rails module ActionMethods @@ -158,60 +153,16 @@ module Rails RESERVED_NAMES = %w[application destroy benchmarker profiler plugin runner test] - class AppGenerator < Base - DATABASES = %w( mysql oracle postgresql sqlite3 frontbase ibm_db ) - JAVASCRIPTS = %w( prototype jquery ) - - attr_accessor :rails_template - add_shebang_option! - - argument :app_path, :type => :string - - class_option :database, :type => :string, :aliases => "-d", :default => "sqlite3", - :desc => "Preconfigure for selected database (options: #{DATABASES.join('/')})" - - class_option :javascript, :type => :string, :aliases => "-j", :default => "prototype", - :desc => "Preconfigure for selected javascript library (options: #{JAVASCRIPTS.join('/')})" - - class_option :builder, :type => :string, :aliases => "-b", - :desc => "Path to an application builder (can be a filesystem path or URL)" - - class_option :template, :type => :string, :aliases => "-m", - :desc => "Path to an application template (can be a filesystem path or URL)" - - class_option :dev, :type => :boolean, :default => false, - :desc => "Setup the application with Gemfile pointing to your Rails checkout" - - class_option :edge, :type => :boolean, :default => false, - :desc => "Setup the application with Gemfile pointing to Rails repository" - - class_option :skip_gemfile, :type => :boolean, :default => false, - :desc => "Don't create a Gemfile" - - class_option :skip_active_record, :type => :boolean, :aliases => "-O", :default => false, - :desc => "Skip Active Record files" - - class_option :skip_test_unit, :type => :boolean, :aliases => "-T", :default => false, - :desc => "Skip Test::Unit files" - - class_option :skip_javascript, :type => :boolean, :aliases => "-J", :default => false, - :desc => "Skip javascript files" - - class_option :skip_git, :type => :boolean, :aliases => "-G", :default => false, - :desc => "Skip Git ignores and keeps" + class AppGenerator < AppBase + add_shared_options_for "application" # Add bin/rails options class_option :version, :type => :boolean, :aliases => "-v", :group => :rails, :desc => "Show Rails version number and quit" - class_option :help, :type => :boolean, :aliases => "-h", :group => :rails, - :desc => "Show this help message and quit" - def initialize(*args) raise Error, "Options should be given after the application name. For details run: rails --help" if args[0].blank? - @original_wd = Dir.pwd - super if !options[:skip_active_record] && !DATABASES.include?(options[:database]) @@ -223,14 +174,7 @@ module Rails end end - def create_root - self.destination_root = File.expand_path(app_path, destination_root) - valid_app_const? - - empty_directory '.' - set_default_accessors! - FileUtils.cd(destination_root) unless options[:pretend] - end + public_task :create_root def create_root_files build(:readme) @@ -309,16 +253,7 @@ module Rails build(:leftovers) end - def apply_rails_template - apply rails_template if rails_template - rescue Thor::Error, LoadError, Errno::ENOENT => e - raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}" - end - - def bundle_if_dev_or_edge - bundle_command = File.basename(Thor::Util.ruby_command).sub(/ruby/, 'bundle') - run "#{bundle_command} install" if dev_or_edge? - end + public_task :apply_rails_template, :bundle_if_dev_or_edge protected @@ -326,40 +261,6 @@ module Rails "rails new #{self.arguments.map(&:usage).join(' ')} [options]" end - def builder - @builder ||= begin - if path = options[:builder] - if URI(path).is_a?(URI::HTTP) - contents = open(path, "Accept" => "application/x-thor-template") {|io| io.read } - else - contents = open(File.expand_path(path, @original_wd)) {|io| io.read } - end - - prok = eval("proc { #{contents} }", TOPLEVEL_BINDING, path, 1) - instance_eval(&prok) - end - - builder_class = defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder - builder_class.send(:include, ActionMethods) - builder_class.new(self) - end - end - - def build(meth, *args) - builder.send(meth, *args) if builder.respond_to?(meth) - end - - def set_default_accessors! - self.rails_template = case options[:template] - when /^http:\/\// - options[:template] - when String - File.expand_path(options[:template], Dir.pwd) - else - options[:template] - end - end - # Define file as an alias to create_file for backwards compatibility. def file(*args, &block) create_file(*args, &block) @@ -388,7 +289,7 @@ module Rails @app_const ||= "#{app_const_base}::Application" end - def valid_app_const? + def valid_const? if app_const =~ /^\d/ raise Error, "Invalid application name #{app_name}. Please give a name which does not start with numbers." elsif RESERVED_NAMES.include?(app_name) @@ -402,28 +303,6 @@ module Rails ActiveSupport::SecureRandom.hex(64) end - def dev_or_edge? - options.dev? || options.edge? - end - - def gem_for_database - # %w( mysql oracle postgresql sqlite3 frontbase ibm_db ) - case options[:database] - when "oracle" then "ruby-oci8" - when "postgresql" then "pg" - when "sqlite3" then "sqlite3-ruby" - when "frontbase" then "ruby-frontbase" - when "mysql" then "mysql2" - else options[:database] - end - end - - def require_for_database - case options[:database] - when "sqlite3" then "sqlite3" - end - end - def mysql_socket @mysql_socket ||= [ "/tmp/mysql.sock", # default @@ -442,6 +321,10 @@ module Rails empty_directory(destination, config) create_file("#{destination}/.gitkeep") unless options[:skip_git] end + + def get_builder_class + defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder + end end end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 40213b1261..86b9e8f40c 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -1,25 +1,8 @@ source 'http://rubygems.org' -<%- if options.dev? -%> -gem 'rails', :path => '<%= Rails::Generators::RAILS_DEV_PATH %>' -gem 'arel', :git => 'git://github.com/rails/arel.git' -gem "rack", :git => "git://github.com/rack/rack.git" -<%- elsif options.edge? -%> -gem 'rails', :git => 'git://github.com/rails/rails.git' -gem 'arel', :git => 'git://github.com/rails/arel.git' -gem "rack", :git => "git://github.com/rack/rack.git" -<%- else -%> -gem 'rails', '<%= Rails::VERSION::STRING %>' +<%= rails_gemfile_entry -%> -# Bundle edge Rails instead: -# gem 'rails', :git => 'git://github.com/rails/rails.git' -# gem 'arel', :git => 'git://github.com/rails/arel.git' -# gem "rack", :git => "git://github.com/rack/rack.git" -<%- end -%> - -<% unless options[:skip_active_record] -%> -gem '<%= gem_for_database %>'<% if require_for_database %>, :require => '<%= require_for_database %>'<% end %> -<% end -%> +<%= database_gemfile_entry -%> # Use unicorn as the web server # gem 'unicorn' diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile index d83cafc3be..4dc1023f1f 100644..100755 --- a/railties/lib/rails/generators/rails/app/templates/Rakefile +++ b/railties/lib/rails/generators/rails/app/templates/Rakefile @@ -1,7 +1,7 @@ +#!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) -require 'rake' <%= app_const %>.load_tasks 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 00a23a7b89..6e515756fe 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -48,6 +48,10 @@ module <%= app_const_base %> # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) <% end -%> +<% if options[:skip_test_unit] -%> + config.generators.test_framework = false +<% end -%> + # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" diff --git a/railties/lib/rails/generators/rails/app/templates/public/javascripts/jquery.js b/railties/lib/rails/generators/rails/app/templates/public/javascripts/jquery.js index ad9a79c433..a4f114586c 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/javascripts/jquery.js +++ b/railties/lib/rails/generators/rails/app/templates/public/javascripts/jquery.js @@ -1,5 +1,5 @@ /*! - * jQuery JavaScript Library v1.4.3 + * jQuery JavaScript Library v1.4.4 * http://jquery.com/ * * Copyright 2010, John Resig @@ -11,7 +11,7 @@ * Copyright 2010, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * - * Date: Thu Oct 14 23:10:06 2010 -0400 + * Date: Thu Nov 11 19:04:53 2010 -0500 */ (function( window, undefined ) { @@ -211,7 +211,7 @@ jQuery.fn = jQuery.prototype = { selector: "", // The current version of jQuery being used - jquery: "1.4.3", + jquery: "1.4.4", // The default length of a jQuery object is 0 length: 0, @@ -330,8 +330,11 @@ jQuery.fn = jQuery.prototype = { jQuery.fn.init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function() { - // copy reference to target object - var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy, copyIsArray; + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { @@ -433,18 +436,21 @@ jQuery.extend({ // If there are functions bound, to execute if ( readyList ) { // Execute all of them - var fn, i = 0; - while ( (fn = readyList[ i++ ]) ) { - fn.call( document, jQuery ); - } + var fn, + i = 0, + ready = readyList; // Reset the list of functions readyList = null; - } - // Trigger any bound ready events - if ( jQuery.fn.triggerHandler ) { - jQuery( document ).triggerHandler( "ready" ); + while ( (fn = ready[ i++ ]) ) { + fn.call( document, jQuery ); + } + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).unbind( "ready" ); + } } } }, @@ -697,7 +703,8 @@ jQuery.extend({ }, merge: function( first, second ) { - var i = first.length, j = 0; + var i = first.length, + j = 0; if ( typeof second.length === "number" ) { for ( var l = second.length; j < l; j++ ) { @@ -964,6 +971,7 @@ return (window.jQuery = window.$ = jQuery); optSelected: opt.selected, // Will be defined later + deleteExpando: true, optDisabled: false, checkClone: false, scriptEval: false, @@ -994,6 +1002,15 @@ return (window.jQuery = window.$ = jQuery); delete window[ id ]; } + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete script.test; + + } catch(e) { + jQuery.support.deleteExpando = false; + } + root.removeChild( script ); if ( div.attachEvent && div.fireEvent ) { @@ -1087,20 +1104,6 @@ return (window.jQuery = window.$ = jQuery); root = script = div = all = a = null; })(); -jQuery.props = { - "for": "htmlFor", - "class": "className", - readonly: "readOnly", - maxlength: "maxLength", - cellspacing: "cellSpacing", - rowspan: "rowSpan", - colspan: "colSpan", - tabindex: "tabIndex", - usemap: "useMap", - frameborder: "frameBorder" -}; - - var windowData = {}, @@ -1237,8 +1240,24 @@ jQuery.extend({ jQuery.fn.extend({ data: function( key, value ) { + var data = null; + if ( typeof key === "undefined" ) { - return this.length ? jQuery.data( this[0] ) : null; + if ( this.length ) { + var attr = this[0].attributes, name; + data = jQuery.data( this[0] ); + + for ( var i = 0, l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = name.substr( 5 ); + dataAttr( this[0], name, data[ name ] ); + } + } + } + + return data; } else if ( typeof key === "object" ) { return this.each(function() { @@ -1250,31 +1269,12 @@ jQuery.fn.extend({ parts[1] = parts[1] ? "." + parts[1] : ""; if ( value === undefined ) { - var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); // Try to fetch any internally stored data first if ( data === undefined && this.length ) { data = jQuery.data( this[0], key ); - - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && this[0].nodeType === 1 ) { - data = this[0].getAttribute( "data-" + key ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - !jQuery.isNaN( data ) ? parseFloat( data ) : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - } else { - data = undefined; - } - } + data = dataAttr( this[0], key, data ); } return data === undefined && parts[1] ? @@ -1283,7 +1283,8 @@ jQuery.fn.extend({ } else { return this.each(function() { - var $this = jQuery( this ), args = [ parts[0], value ]; + var $this = jQuery( this ), + args = [ parts[0], value ]; $this.triggerHandler( "setData" + parts[1] + "!", args ); jQuery.data( this, key, value ); @@ -1299,6 +1300,33 @@ jQuery.fn.extend({ } }); +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + data = elem.getAttribute( "data-" + key ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + !jQuery.isNaN( data ) ? parseFloat( data ) : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + @@ -1329,7 +1357,8 @@ jQuery.extend({ dequeue: function( elem, type ) { type = type || "fx"; - var queue = jQuery.queue( elem, type ), fn = queue.shift(); + var queue = jQuery.queue( elem, type ), + fn = queue.shift(); // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { @@ -1405,6 +1434,19 @@ var rclass = /[\n\t]/g, rclickable = /^a(?:rea)?$/i, rradiocheck = /^(?:radio|checkbox)$/i; +jQuery.props = { + "for": "htmlFor", + "class": "className", + readonly: "readOnly", + maxlength: "maxLength", + cellspacing: "cellSpacing", + rowspan: "rowSpan", + colspan: "colSpan", + tabindex: "tabIndex", + usemap: "useMap", + frameborder: "frameBorder" +}; + jQuery.fn.extend({ attr: function( name, value ) { return jQuery.access( this, name, value, true, jQuery.attr ); @@ -1438,7 +1480,9 @@ jQuery.fn.extend({ elem.className = value; } else { - var className = " " + elem.className + " ", setClass = elem.className; + var className = " " + elem.className + " ", + setClass = elem.className; + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { setClass += " " + classNames[c]; @@ -1486,7 +1530,8 @@ jQuery.fn.extend({ }, toggleClass: function( value, stateVal ) { - var type = typeof value, isBool = typeof stateVal === "boolean"; + var type = typeof value, + isBool = typeof stateVal === "boolean"; if ( jQuery.isFunction( value ) ) { return this.each(function(i) { @@ -1498,7 +1543,9 @@ jQuery.fn.extend({ return this.each(function() { if ( type === "string" ) { // toggle individual class names - var className, i = 0, self = jQuery(this), + var className, + i = 0, + self = jQuery( this ), state = stateVal, classNames = value.split( rspaces ); @@ -1667,91 +1714,88 @@ jQuery.extend({ // Try to normalize/fix the name name = notxml && jQuery.props[ name ] || name; - // Only do all the following if this is a node (faster for style) - if ( elem.nodeType === 1 ) { - // These attributes require special treatment - var special = rspecialurl.test( name ); - - // Safari mis-reports the default selected property of an option - // Accessing the parent's selectedIndex property fixes it - if ( name === "selected" && !jQuery.support.optSelected ) { - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } + // These attributes require special treatment + var special = rspecialurl.test( name ); - // If applicable, access the attribute via the DOM 0 way - // 'in' checks fail in Blackberry 4.7 #6931 - if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) { - if ( set ) { - // We can't allow the type property to be changed (since it causes problems in IE) - if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { - jQuery.error( "type property can't be changed" ); - } + // Safari mis-reports the default selected property of an option + // Accessing the parent's selectedIndex property fixes it + if ( name === "selected" && !jQuery.support.optSelected ) { + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; - if ( value === null ) { - if ( elem.nodeType === 1 ) { - elem.removeAttribute( name ); - } - - } else { - elem[ name ] = value; - } + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; } + } + } - // browsers index elements by id/name on forms, give priority to attributes. - if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { - return elem.getAttributeNode( name ).nodeValue; + // If applicable, access the attribute via the DOM 0 way + // 'in' checks fail in Blackberry 4.7 #6931 + if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) { + if ( set ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); } - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - if ( name === "tabIndex" ) { - var attributeNode = elem.getAttributeNode( "tabIndex" ); + if ( value === null ) { + if ( elem.nodeType === 1 ) { + elem.removeAttribute( name ); + } - return attributeNode && attributeNode.specified ? - attributeNode.value : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; + } else { + elem[ name ] = value; } + } - return elem[ name ]; + // browsers index elements by id/name on forms, give priority to attributes. + if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { + return elem.getAttributeNode( name ).nodeValue; } - if ( !jQuery.support.style && notxml && name === "style" ) { - if ( set ) { - elem.style.cssText = "" + value; - } + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + if ( name === "tabIndex" ) { + var attributeNode = elem.getAttributeNode( "tabIndex" ); - return elem.style.cssText; + return attributeNode && attributeNode.specified ? + attributeNode.value : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; } + return elem[ name ]; + } + + if ( !jQuery.support.style && notxml && name === "style" ) { if ( set ) { - // convert the value to a string (all browsers do this but IE) see #1070 - elem.setAttribute( name, "" + value ); + elem.style.cssText = "" + value; } - // Ensure that missing attributes return undefined - // Blackberry 4.7 returns "" from getAttribute #6938 - if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) { - return undefined; - } + return elem.style.cssText; + } - var attr = !jQuery.support.hrefNormalized && notxml && special ? - // Some attributes require a special call on IE - elem.getAttribute( name, 2 ) : - elem.getAttribute( name ); + if ( set ) { + // convert the value to a string (all browsers do this but IE) see #1070 + elem.setAttribute( name, "" + value ); + } - // Non-existent attributes return null, we normalize to undefined - return attr === null ? undefined : attr; + // Ensure that missing attributes return undefined + // Blackberry 4.7 returns "" from getAttribute #6938 + if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) { + return undefined; } + + var attr = !jQuery.support.hrefNormalized && notxml && special ? + // Some attributes require a special call on IE + elem.getAttribute( name, 2 ) : + elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return attr === null ? undefined : attr; } }); @@ -1790,6 +1834,9 @@ jQuery.event = { if ( handler === false ) { handler = returnFalse; + } else if ( !handler ) { + // Fixes bug #7229. Fix recommended by jdalton + return; } var handleObjIn, handleObj; @@ -2133,8 +2180,10 @@ jQuery.event = { jQuery.event.trigger( event, data, parent, true ); } else if ( !event.isDefaultPrevented() ) { - var target = event.target, old, targetType = type.replace(rnamespaces, ""), - isClick = jQuery.nodeName(target, "a") && targetType === "click", + var old, + target = event.target, + targetType = type.replace( rnamespaces, "" ), + isClick = jQuery.nodeName( target, "a" ) && targetType === "click", special = jQuery.event.special[ targetType ] || {}; if ( (!special._default || special._default.call( elem, event ) === false) && @@ -2166,7 +2215,9 @@ jQuery.event = { }, handle: function( event ) { - var all, handlers, namespaces, namespace_sort = [], namespace_re, events, args = jQuery.makeArray( arguments ); + var all, handlers, namespaces, namespace_re, events, + namespace_sort = [], + args = jQuery.makeArray( arguments ); event = args[0] = jQuery.event.fix( event || window.event ); event.currentTarget = this; @@ -2245,7 +2296,8 @@ jQuery.event = { // Fix target property, if necessary if ( !event.target ) { - event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either + // Fixes #1925 where srcElement might not be defined either + event.target = event.srcElement || document; } // check if target is a textnode (safari) @@ -2260,7 +2312,9 @@ jQuery.event = { // Calculate pageX/Y if missing and clientX/Y available if ( event.pageX == null && event.clientX != null ) { - var doc = document.documentElement, body = document.body; + var doc = document.documentElement, + body = document.body; + event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); } @@ -2466,7 +2520,8 @@ if ( !jQuery.support.submitBubbles ) { setup: function( data, namespaces ) { if ( this.nodeName.toLowerCase() !== "form" ) { jQuery.event.add(this, "click.specialSubmit", function( e ) { - var elem = e.target, type = elem.type; + var elem = e.target, + type = elem.type; if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { e.liveFired = undefined; @@ -2475,7 +2530,8 @@ if ( !jQuery.support.submitBubbles ) { }); jQuery.event.add(this, "keypress.specialSubmit", function( e ) { - var elem = e.target, type = elem.type; + var elem = e.target, + type = elem.type; if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { e.liveFired = undefined; @@ -2716,7 +2772,8 @@ jQuery.fn.extend({ toggle: function( fn ) { // Save reference to arguments for access in closure - var args = arguments, i = 1; + var args = arguments, + i = 1; // link all the functions, so any of them can unbind this click handler while ( i < args.length ) { @@ -2811,8 +2868,9 @@ jQuery.each(["live", "die"], function( i, name ) { }); function liveHandler( event ) { - var stop, maxLevel, elems = [], selectors = [], - related, match, handleObj, elem, j, i, l, data, close, namespace, ret, + var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret, + elems = [], + selectors = [], events = jQuery.data( this, this.nodeType ? "events" : "__events__" ); if ( typeof events === "function" ) { @@ -2823,7 +2881,7 @@ function liveHandler( event ) { if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) { return; } - + if ( event.namespace ) { namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); } @@ -2887,6 +2945,9 @@ function liveHandler( event ) { if ( ret === false ) { stop = false; } + if ( event.isImmediatePropagationStopped() ) { + break; + } } } @@ -2954,12 +3015,12 @@ var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[ // optimization where it does not always call our comparision // function. If that is the case, discard the hasDuplicate value. // Thus far that includes Google Chrome. -[0, 0].sort(function(){ +[0, 0].sort(function() { baseHasDuplicate = false; return 0; }); -var Sizzle = function(selector, context, results, seed) { +var Sizzle = function( selector, context, results, seed ) { results = results || []; context = context || document; @@ -2973,13 +3034,16 @@ var Sizzle = function(selector, context, results, seed) { return results; } - var parts = [], m, set, checkSet, extra, prune = true, contextXML = Sizzle.isXML(context), - soFar = selector, ret, cur, pop, i; + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; // Reset the position of the chunker regexp (start from head) do { - chunker.exec(""); - m = chunker.exec(soFar); + chunker.exec( "" ); + m = chunker.exec( soFar ); if ( m ) { soFar = m[3]; @@ -2994,8 +3058,10 @@ var Sizzle = function(selector, context, results, seed) { } while ( m ); if ( parts.length > 1 && origPOS.exec( selector ) ) { + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { set = posProcess( parts[0] + parts[1], context ); + } else { set = Expr.relative[ parts[0] ] ? [ context ] : @@ -3011,23 +3077,31 @@ var Sizzle = function(selector, context, results, seed) { set = posProcess( selector, set ); } } + } else { // Take a shortcut and set the context if the root selector is an ID // (but not if it'll be faster if the inner selector is an ID) if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; } if ( context ) { ret = seed ? { expr: parts.pop(), set: makeArray(seed) } : Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; if ( parts.length > 0 ) { - checkSet = makeArray(set); + checkSet = makeArray( set ); + } else { prune = false; } @@ -3048,6 +3122,7 @@ var Sizzle = function(selector, context, results, seed) { Expr.relative[ cur ]( checkSet, pop, contextXML ); } + } else { checkSet = parts = []; } @@ -3064,12 +3139,14 @@ var Sizzle = function(selector, context, results, seed) { if ( toString.call(checkSet) === "[object Array]" ) { if ( !prune ) { results.push.apply( results, checkSet ); + } else if ( context && context.nodeType === 1 ) { for ( i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { results.push( set[i] ); } } + } else { for ( i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && checkSet[i].nodeType === 1 ) { @@ -3077,6 +3154,7 @@ var Sizzle = function(selector, context, results, seed) { } } } + } else { makeArray( checkSet, results ); } @@ -3089,15 +3167,15 @@ var Sizzle = function(selector, context, results, seed) { return results; }; -Sizzle.uniqueSort = function(results){ +Sizzle.uniqueSort = function( results ) { if ( sortOrder ) { hasDuplicate = baseHasDuplicate; - results.sort(sortOrder); + results.sort( sortOrder ); if ( hasDuplicate ) { for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[i-1] ) { - results.splice(i--, 1); + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); } } } @@ -3106,15 +3184,15 @@ Sizzle.uniqueSort = function(results){ return results; }; -Sizzle.matches = function(expr, set){ - return Sizzle(expr, null, null, set); +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); }; -Sizzle.matchesSelector = function(node, expr){ - return Sizzle(expr, null, null, [node]).length > 0; +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; }; -Sizzle.find = function(expr, context, isXML){ +Sizzle.find = function( expr, context, isXML ) { var set; if ( !expr ) { @@ -3122,15 +3200,17 @@ Sizzle.find = function(expr, context, isXML){ } for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var type = Expr.order[i], match; + var match, + type = Expr.order[i]; if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { var left = match[1]; - match.splice(1,1); + match.splice( 1, 1 ); if ( left.substr( left.length - 1 ) !== "\\" ) { match[1] = (match[1] || "").replace(/\\/g, ""); set = Expr.find[ type ]( match, context, isXML ); + if ( set != null ) { expr = expr.replace( Expr.match[ type ], "" ); break; @@ -3140,20 +3220,26 @@ Sizzle.find = function(expr, context, isXML){ } if ( !set ) { - set = context.getElementsByTagName("*"); + set = context.getElementsByTagName( "*" ); } - return {set: set, expr: expr}; + return { set: set, expr: expr }; }; -Sizzle.filter = function(expr, set, inplace, not){ - var old = expr, result = [], curLoop = set, match, anyFound, - isXMLFilter = set && set[0] && Sizzle.isXML(set[0]); +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); while ( expr && set.length ) { for ( var type in Expr.filter ) { if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - var filter = Expr.filter[ type ], found, item, left = match[1]; + var found, item, + filter = Expr.filter[ type ], + left = match[1]; + anyFound = false; match.splice(1,1); @@ -3171,6 +3257,7 @@ Sizzle.filter = function(expr, set, inplace, not){ if ( !match ) { anyFound = found = true; + } else if ( match === true ) { continue; } @@ -3185,9 +3272,11 @@ Sizzle.filter = function(expr, set, inplace, not){ if ( inplace && found != null ) { if ( pass ) { anyFound = true; + } else { curLoop[i] = false; } + } else if ( pass ) { result.push( item ); anyFound = true; @@ -3216,6 +3305,7 @@ Sizzle.filter = function(expr, set, inplace, not){ if ( expr === old ) { if ( anyFound == null ) { Sizzle.error( expr ); + } else { break; } @@ -3233,6 +3323,7 @@ Sizzle.error = function( msg ) { var Expr = Sizzle.selectors = { order: [ "ID", "NAME", "TAG" ], + match: { ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, @@ -3243,20 +3334,24 @@ var Expr = Sizzle.selectors = { POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ }, + leftMatch: {}, + attrMap: { "class": "className", "for": "htmlFor" }, + attrHandle: { - href: function(elem){ - return elem.getAttribute("href"); + href: function( elem ) { + return elem.getAttribute( "href" ); } }, + relative: { "+": function(checkSet, part){ var isPartStr = typeof part === "string", - isTag = isPartStr && !/\W/.test(part), + isTag = isPartStr && !/\W/.test( part ), isPartStrNotTag = isPartStr && !isTag; if ( isTag ) { @@ -3277,23 +3372,29 @@ var Expr = Sizzle.selectors = { Sizzle.filter( part, checkSet, true ); } }, - ">": function(checkSet, part){ - var isPartStr = typeof part === "string", - elem, i = 0, l = checkSet.length; - if ( isPartStr && !/\W/.test(part) ) { + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !/\W/.test( part ) ) { part = part.toLowerCase(); for ( ; i < l; i++ ) { elem = checkSet[i]; + if ( elem ) { var parent = elem.parentNode; checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; } } + } else { for ( ; i < l; i++ ) { elem = checkSet[i]; + if ( elem ) { checkSet[i] = isPartStr ? elem.parentNode : @@ -3306,8 +3407,11 @@ var Expr = Sizzle.selectors = { } } }, + "": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck, nodeCheck; + var nodeCheck, + doneName = done++, + checkFn = dirCheck; if ( typeof part === "string" && !/\W/.test(part) ) { part = part.toLowerCase(); @@ -3315,22 +3419,26 @@ var Expr = Sizzle.selectors = { checkFn = dirNodeCheck; } - checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); }, - "~": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck, nodeCheck; - if ( typeof part === "string" && !/\W/.test(part) ) { + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !/\W/.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; } - checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); } }, + find: { - ID: function(match, context, isXML){ + ID: function( match, context, isXML ) { if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); // Check parentNode to catch when Blackberry 4.6 returns @@ -3338,9 +3446,11 @@ var Expr = Sizzle.selectors = { return m && m.parentNode ? [m] : []; } }, - NAME: function(match, context){ + + NAME: function( match, context ) { if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], results = context.getElementsByName(match[1]); + var ret = [], + results = context.getElementsByName( match[1] ); for ( var i = 0, l = results.length; i < l; i++ ) { if ( results[i].getAttribute("name") === match[1] ) { @@ -3351,12 +3461,13 @@ var Expr = Sizzle.selectors = { return ret.length === 0 ? null : ret; } }, - TAG: function(match, context){ - return context.getElementsByTagName(match[1]); + + TAG: function( match, context ) { + return context.getElementsByTagName( match[1] ); } }, preFilter: { - CLASS: function(match, curLoop, inplace, result, not, isXML){ + CLASS: function( match, curLoop, inplace, result, not, isXML ) { match = " " + match[1].replace(/\\/g, "") + " "; if ( isXML ) { @@ -3369,6 +3480,7 @@ var Expr = Sizzle.selectors = { if ( !inplace ) { result.push( elem ); } + } else if ( inplace ) { curLoop[i] = false; } @@ -3377,13 +3489,16 @@ var Expr = Sizzle.selectors = { return false; }, - ID: function(match){ + + ID: function( match ) { return match[1].replace(/\\/g, ""); }, - TAG: function(match, curLoop){ + + TAG: function( match, curLoop ) { return match[1].toLowerCase(); }, - CHILD: function(match){ + + CHILD: function( match ) { if ( match[1] === "nth" ) { // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( @@ -3400,7 +3515,8 @@ var Expr = Sizzle.selectors = { return match; }, - ATTR: function(match, curLoop, inplace, result, not, isXML){ + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { var name = match[1].replace(/\\/g, ""); if ( !isXML && Expr.attrMap[name] ) { @@ -3413,122 +3529,156 @@ var Expr = Sizzle.selectors = { return match; }, - PSEUDO: function(match, curLoop, inplace, result, not){ + + PSEUDO: function( match, curLoop, inplace, result, not ) { if ( match[1] === "not" ) { // If we're dealing with a complex expression, or a simple one if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { match[3] = Sizzle(match[3], null, null, curLoop); + } else { var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + if ( !inplace ) { result.push.apply( result, ret ); } + return false; } + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { return true; } return match; }, - POS: function(match){ + + POS: function( match ) { match.unshift( true ); + return match; } }, + filters: { - enabled: function(elem){ + enabled: function( elem ) { return elem.disabled === false && elem.type !== "hidden"; }, - disabled: function(elem){ + + disabled: function( elem ) { return elem.disabled === true; }, - checked: function(elem){ + + checked: function( elem ) { return elem.checked === true; }, - selected: function(elem){ + + selected: function( elem ) { // Accessing this property makes selected-by-default // options in Safari work properly elem.parentNode.selectedIndex; + return elem.selected === true; }, - parent: function(elem){ + + parent: function( elem ) { return !!elem.firstChild; }, - empty: function(elem){ + + empty: function( elem ) { return !elem.firstChild; }, - has: function(elem, i, match){ + + has: function( elem, i, match ) { return !!Sizzle( match[3], elem ).length; }, - header: function(elem){ + + header: function( elem ) { return (/h\d/i).test( elem.nodeName ); }, - text: function(elem){ + + text: function( elem ) { return "text" === elem.type; }, - radio: function(elem){ + radio: function( elem ) { return "radio" === elem.type; }, - checkbox: function(elem){ + + checkbox: function( elem ) { return "checkbox" === elem.type; }, - file: function(elem){ + + file: function( elem ) { return "file" === elem.type; }, - password: function(elem){ + password: function( elem ) { return "password" === elem.type; }, - submit: function(elem){ + + submit: function( elem ) { return "submit" === elem.type; }, - image: function(elem){ + + image: function( elem ) { return "image" === elem.type; }, - reset: function(elem){ + + reset: function( elem ) { return "reset" === elem.type; }, - button: function(elem){ + + button: function( elem ) { return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; }, - input: function(elem){ - return (/input|select|textarea|button/i).test(elem.nodeName); + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); } }, setFilters: { - first: function(elem, i){ + first: function( elem, i ) { return i === 0; }, - last: function(elem, i, match, array){ + + last: function( elem, i, match, array ) { return i === array.length - 1; }, - even: function(elem, i){ + + even: function( elem, i ) { return i % 2 === 0; }, - odd: function(elem, i){ + + odd: function( elem, i ) { return i % 2 === 1; }, - lt: function(elem, i, match){ + + lt: function( elem, i, match ) { return i < match[3] - 0; }, - gt: function(elem, i, match){ + + gt: function( elem, i, match ) { return i > match[3] - 0; }, - nth: function(elem, i, match){ + + nth: function( elem, i, match ) { return match[3] - 0 === i; }, - eq: function(elem, i, match){ + + eq: function( elem, i, match ) { return match[3] - 0 === i; } }, filter: { - PSEUDO: function(elem, match, i, array){ - var name = match[1], filter = Expr.filters[ name ]; + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; if ( filter ) { return filter( elem, i, match, array ); + } else if ( name === "contains" ) { return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + } else if ( name === "not" ) { var not = match[3]; @@ -3539,33 +3689,43 @@ var Expr = Sizzle.selectors = { } return true; + } else { Sizzle.error( "Syntax error, unrecognized expression: " + name ); } }, - CHILD: function(elem, match){ - var type = match[1], node = elem; - switch (type) { - case 'only': - case 'first': + + CHILD: function( elem, match ) { + var type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": while ( (node = node.previousSibling) ) { if ( node.nodeType === 1 ) { return false; } } + if ( type === "first" ) { return true; } + node = elem; - case 'last': + + case "last": while ( (node = node.nextSibling) ) { if ( node.nodeType === 1 ) { return false; } } + return true; - case 'nth': - var first = match[2], last = match[3]; + + case "nth": + var first = match[2], + last = match[3]; if ( first === 1 && last === 0 ) { return true; @@ -3576,33 +3736,41 @@ var Expr = Sizzle.selectors = { if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { var count = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { if ( node.nodeType === 1 ) { node.nodeIndex = ++count; } } + parent.sizcache = doneName; } var diff = elem.nodeIndex - last; + if ( first === 0 ) { return diff === 0; + } else { return ( diff % first === 0 && diff / first >= 0 ); } } }, - ID: function(elem, match){ + + ID: function( elem, match ) { return elem.nodeType === 1 && elem.getAttribute("id") === match; }, - TAG: function(elem, match){ + + TAG: function( elem, match ) { return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; }, - CLASS: function(elem, match){ + + CLASS: function( elem, match ) { return (" " + (elem.className || elem.getAttribute("class")) + " ") .indexOf( match ) > -1; }, - ATTR: function(elem, match){ + + ATTR: function( elem, match ) { var name = match[1], result = Expr.attrHandle[ name ] ? Expr.attrHandle[ name ]( elem ) : @@ -3633,8 +3801,10 @@ var Expr = Sizzle.selectors = { value === check || value.substr(0, check.length + 1) === check + "-" : false; }, - POS: function(elem, match, i, array){ - var name = match[2], filter = Expr.setFilters[ name ]; + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; if ( filter ) { return filter( elem, i, match, array ); @@ -3653,7 +3823,7 @@ for ( var type in Expr.match ) { Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); } -var makeArray = function(array, results) { +var makeArray = function( array, results ) { array = Array.prototype.slice.call( array, 0 ); if ( results ) { @@ -3672,17 +3842,20 @@ try { Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; // Provide a fallback method if it does not work -} catch(e){ - makeArray = function(array, results) { - var ret = results || [], i = 0; +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; if ( toString.call(array) === "[object Array]" ) { Array.prototype.push.apply( ret, array ); + } else { if ( typeof array.length === "number" ) { for ( var l = array.length; i < l; i++ ) { ret.push( array[i] ); } + } else { for ( ; array[i]; i++ ) { ret.push( array[i] ); @@ -3709,10 +3882,15 @@ if ( document.documentElement.compareDocumentPosition ) { return a.compareDocumentPosition(b) & 4 ? -1 : 1; }; + } else { sortOrder = function( a, b ) { - var ap = [], bp = [], aup = a.parentNode, bup = b.parentNode, - cur = aup, al, bl; + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; // The nodes are identical, we can exit early if ( a === b ) { @@ -3805,31 +3983,40 @@ Sizzle.getText = function( elems ) { (function(){ // We're going to inject a fake input element with a specified name var form = document.createElement("div"), - id = "script" + (new Date()).getTime(); + id = "script" + (new Date()).getTime(), + root = document.documentElement; + form.innerHTML = "<a name='" + id + "'/>"; // Inject it into the root element, check its status, and remove it quickly - var root = document.documentElement; root.insertBefore( form, root.firstChild ); // The workaround has to do additional checks after a getElementById // Which slows things down for other browsers (hence the branching) if ( document.getElementById( id ) ) { - Expr.find.ID = function(match, context, isXML){ + Expr.find.ID = function( match, context, isXML ) { if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); - return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; } }; - Expr.filter.ID = function(elem, match){ + Expr.filter.ID = function( elem, match ) { var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + return elem.nodeType === 1 && node && node.nodeValue === match; }; } root.removeChild( form ); - root = form = null; // release memory in IE + + // release memory in IE + root = form = null; })(); (function(){ @@ -3842,8 +4029,8 @@ Sizzle.getText = function( elems ) { // Make sure no comments are found if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function(match, context){ - var results = context.getElementsByTagName(match[1]); + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); // Filter out possible comments if ( match[1] === "*" ) { @@ -3864,19 +4051,25 @@ Sizzle.getText = function( elems ) { // Check to see if an attribute returns normalized href attributes div.innerHTML = "<a href='#'></a>"; + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && div.firstChild.getAttribute("href") !== "#" ) { - Expr.attrHandle.href = function(elem){ - return elem.getAttribute("href", 2); + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); }; } - div = null; // release memory in IE + // release memory in IE + div = null; })(); if ( document.querySelectorAll ) { (function(){ - var oldSizzle = Sizzle, div = document.createElement("div"); + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + div.innerHTML = "<p class='TEST'></p>"; // Safari can't handle uppercase or unicode characters when @@ -3885,9 +4078,12 @@ if ( document.querySelectorAll ) { return; } - Sizzle = function(query, context, extra, seed){ + Sizzle = function( query, context, extra, seed ) { context = context || document; + // Make sure that attribute selectors are quoted + query = query.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + // Only use querySelectorAll on non-XML documents // (ID selectors don't work in non-HTML documents) if ( !seed && !Sizzle.isXML(context) ) { @@ -3901,17 +4097,19 @@ if ( document.querySelectorAll ) { // and working up from there (Thanks to Andrew Dupont for the technique) // IE 8 doesn't work on object elements } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - var old = context.id, id = context.id = "__sizzle__"; + var old = context.getAttribute( "id" ), + nid = old || id; + + if ( !old ) { + context.setAttribute( "id", nid ); + } try { - return makeArray( context.querySelectorAll( "#" + id + " " + query ), extra ); + return makeArray( context.querySelectorAll( "#" + nid + " " + query ), extra ); } catch(pseudoError) { } finally { - if ( old ) { - context.id = old; - - } else { + if ( !old ) { context.removeAttribute( "id" ); } } @@ -3925,7 +4123,8 @@ if ( document.querySelectorAll ) { Sizzle[ prop ] = oldSizzle[ prop ]; } - div = null; // release memory in IE + // release memory in IE + div = null; })(); } @@ -3937,7 +4136,7 @@ if ( document.querySelectorAll ) { try { // This should fail with an exception // Gecko does not error, returns false instead - matches.call( document.documentElement, ":sizzle" ); + matches.call( document.documentElement, "[test!='']:sizzle" ); } catch( pseudoError ) { pseudoWorks = true; @@ -3945,13 +4144,18 @@ if ( document.querySelectorAll ) { if ( matches ) { Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { try { - if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) ) { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { return matches.call( node, expr ); } } catch(e) {} + } - return Sizzle(expr, null, null, [node]).length > 0; + return Sizzle(expr, null, null, [node]).length > 0; }; } })(); @@ -3975,22 +4179,25 @@ if ( document.querySelectorAll ) { } Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function(match, context, isXML) { + Expr.find.CLASS = function( match, context, isXML ) { if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { return context.getElementsByClassName(match[1]); } }; - div = null; // release memory in IE + // release memory in IE + div = null; })(); function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; + if ( elem ) { - elem = elem[dir]; var match = false; + elem = elem[dir]; + while ( elem ) { if ( elem.sizcache === doneName ) { match = checkSet[elem.sizset]; @@ -4018,9 +4225,11 @@ function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; + if ( elem ) { - elem = elem[dir]; var match = false; + + elem = elem[dir]; while ( elem ) { if ( elem.sizcache === doneName ) { @@ -4033,6 +4242,7 @@ function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { elem.sizcache = doneName; elem.sizset = i; } + if ( typeof cur !== "string" ) { if ( elem === cur ) { match = true; @@ -4053,21 +4263,34 @@ function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { } } -Sizzle.contains = document.documentElement.contains ? function(a, b){ - return a !== b && (a.contains ? a.contains(b) : true); -} : function(a, b){ - return !!(a.compareDocumentPosition(b) & 16); -}; +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; -Sizzle.isXML = function(elem){ +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { // documentElement is verified for cases where it doesn't yet exist // (such as loading iframes in IE - #4833) var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; }; -var posProcess = function(selector, context){ - var tmpSet = [], later = "", match, +var posProcess = function( selector, context ) { + var match, + tmpSet = [], + later = "", root = context.nodeType ? [context] : context; // Position selectors must be done after the filter @@ -4109,7 +4332,8 @@ var runtil = /Until$/, jQuery.fn.extend({ find: function( selector ) { - var ret = this.pushStack( "", "find", selector ), length = 0; + var ret = this.pushStack( "", "find", selector ), + length = 0; for ( var i = 0, l = this.length; i < l; i++ ) { length = ret.length; @@ -4158,7 +4382,9 @@ jQuery.fn.extend({ var ret = [], i, l, cur = this[0]; if ( jQuery.isArray( selectors ) ) { - var match, matches = {}, selector, level = 1; + var match, selector, + matches = {}, + level = 1; if ( cur && selectors.length ) { for ( i = 0, l = selectors.length; i < l; i++ ) { @@ -4324,7 +4550,9 @@ jQuery.extend({ }, dir: function( elem, dir, until ) { - var matched = [], cur = elem[dir]; + var matched = [], + cur = elem[ dir ]; + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { if ( cur.nodeType === 1 ) { matched.push( cur ); @@ -4400,7 +4628,8 @@ var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, rtbody = /<tbody/i, rhtml = /<|&#?\w+;/, rnocache = /<(?:script|object|embed|option|style)/i, - rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, // checked="checked" or checked (html5) + // checked="checked" or checked (html5) + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, raction = /\=([^="'>\s]+\/)>/g, wrapMap = { option: [ 1, "<select multiple='multiple'>", "</select>" ], @@ -4426,7 +4655,8 @@ jQuery.fn.extend({ text: function( text ) { if ( jQuery.isFunction(text) ) { return this.each(function(i) { - var self = jQuery(this); + var self = jQuery( this ); + self.text( text.call(this, i, self.text()) ); }); } @@ -4475,7 +4705,8 @@ jQuery.fn.extend({ } return this.each(function() { - var self = jQuery( this ), contents = self.contents(); + var self = jQuery( this ), + contents = self.contents(); if ( contents.length ) { contents.wrapAll( html ); @@ -4586,7 +4817,9 @@ jQuery.fn.extend({ // attributes in IE that are actually only stored // as properties will not be copied (such as the // the name attribute on an input). - var html = this.outerHTML, ownerDocument = this.ownerDocument; + var html = this.outerHTML, + ownerDocument = this.ownerDocument; + if ( !html ) { var div = ownerDocument.createElement("div"); div.appendChild( this.cloneNode(true) ); @@ -4641,7 +4874,8 @@ jQuery.fn.extend({ } else if ( jQuery.isFunction( value ) ) { this.each(function(i){ - var self = jQuery(this); + var self = jQuery( this ); + self.html( value.call(this, i, self.html()) ); }); @@ -4664,13 +4898,14 @@ jQuery.fn.extend({ } if ( typeof value !== "string" ) { - value = jQuery(value).detach(); + value = jQuery( value ).detach(); } return this.each(function() { - var next = this.nextSibling, parent = this.parentNode; + var next = this.nextSibling, + parent = this.parentNode; - jQuery(this).remove(); + jQuery( this ).remove(); if ( next ) { jQuery(next).before( value ); @@ -4688,7 +4923,9 @@ jQuery.fn.extend({ }, domManip: function( args, table, callback ) { - var results, first, value = args[0], scripts = [], fragment, parent; + var results, first, fragment, parent, + value = args[0], + scripts = []; // We can't cloneNode fragments that contain checked, in WebKit if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) { @@ -4763,7 +5000,9 @@ function cloneCopyEvent(orig, ret) { return; } - var oldData = jQuery.data( orig[i++] ), curData = jQuery.data( this, oldData ), events = oldData && oldData.events; + var oldData = jQuery.data( orig[i++] ), + curData = jQuery.data( this, oldData ), + events = oldData && oldData.events; if ( events ) { delete curData.handle; @@ -4820,7 +5059,8 @@ jQuery.each({ replaceAll: "replaceWith" }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { - var ret = [], insert = jQuery( selector ), + var ret = [], + insert = jQuery( selector ), parent = this.length === 1 && this[0].parentNode; if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { @@ -5004,8 +5244,8 @@ var ralpha = /alpha\([^)]*\)/i, cssHeight = [ "Top", "Bottom" ], curCSS, - // cache check for defaultView.getComputedStyle - getComputedStyle = document.defaultView && document.defaultView.getComputedStyle, + getComputedStyle, + currentStyle, fcamelCase = function( all, letter ) { return letter.toUpperCase(); @@ -5161,7 +5401,29 @@ jQuery.each(["height", "width"], function( i, name ) { }); } - return val + "px"; + if ( val <= 0 ) { + val = curCSS( elem, name, name ); + + if ( val === "0px" && currentStyle ) { + val = currentStyle( elem, name, name ); + } + + if ( val != null ) { + // Should return "auto" instead of 0, use 0 for + // temporary backwards-compat + return val === "" || val === "auto" ? "0px" : val; + } + } + + if ( val < 0 || val == null ) { + val = elem.style[ name ]; + + // Should return "auto" instead of 0, use 0 for + // temporary backwards-compat + return val === "" || val === "auto" ? "0px" : val; + } + + return typeof val === "string" ? val : val + "px"; } }, @@ -5210,8 +5472,8 @@ if ( !jQuery.support.opacity ) { }; } -if ( getComputedStyle ) { - curCSS = function( elem, newName, name ) { +if ( document.defaultView && document.defaultView.getComputedStyle ) { + getComputedStyle = function( elem, newName, name ) { var ret, defaultView, computedStyle; name = name.replace( rupper, "-$1" ).toLowerCase(); @@ -5229,10 +5491,13 @@ if ( getComputedStyle ) { return ret; }; +} -} else if ( document.documentElement.currentStyle ) { - curCSS = function( elem, name ) { - var left, rsLeft, ret = elem.currentStyle && elem.currentStyle[ name ], style = elem.style; +if ( document.documentElement.currentStyle ) { + currentStyle = function( elem, name ) { + var left, rsLeft, + ret = elem.currentStyle && elem.currentStyle[ name ], + style = elem.style; // From the awesome hack by Dean Edwards // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 @@ -5254,10 +5519,12 @@ if ( getComputedStyle ) { elem.runtimeStyle.left = rsLeft; } - return ret; + return ret === "" ? "auto" : ret; }; } +curCSS = getComputedStyle || currentStyle; + function getWH( elem, name, extra ) { var which = name === "width" ? cssWidth : cssHeight, val = name === "width" ? elem.offsetWidth : elem.offsetHeight; @@ -5284,7 +5551,8 @@ function getWH( elem, name, extra ) { if ( jQuery.expr && jQuery.expr.filters ) { jQuery.expr.filters.hidden = function( elem ) { - var width = elem.offsetWidth, height = elem.offsetHeight; + var width = elem.offsetWidth, + height = elem.offsetHeight; return (width === 0 && height === 0) || (!jQuery.support.reliableHiddenOffsets && (elem.style.display || jQuery.css( elem, "display" )) === "none"); }; @@ -5301,7 +5569,7 @@ var jsc = jQuery.now(), rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, rselectTextarea = /^(?:select|textarea)/i, rinput = /^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, - rnoContent = /^(?:GET|HEAD|DELETE)$/, + rnoContent = /^(?:GET|HEAD)$/, rbracket = /\[\]$/, jsre = /\=\?(&|$)/, rquery = /\?/, @@ -5536,10 +5804,6 @@ jQuery.extend({ var customJsonp = window[ jsonp ]; window[ jsonp ] = function( tmp ) { - data = tmp; - jQuery.handleSuccess( s, xhr, status, data ); - jQuery.handleComplete( s, xhr, status, data ); - if ( jQuery.isFunction( customJsonp ) ) { customJsonp( tmp ); @@ -5551,6 +5815,10 @@ jQuery.extend({ delete window[ jsonp ]; } catch( jsonpError ) {} } + + data = tmp; + jQuery.handleSuccess( s, xhr, status, data ); + jQuery.handleComplete( s, xhr, status, data ); if ( head ) { head.removeChild( script ); @@ -5562,7 +5830,7 @@ jQuery.extend({ s.cache = false; } - if ( s.cache === false && type === "GET" ) { + if ( s.cache === false && noContent ) { var ts = jQuery.now(); // try replacing _= if it is there @@ -5572,8 +5840,8 @@ jQuery.extend({ s.url = ret + ((ret === s.url) ? (rquery.test(s.url) ? "&" : "?") + "_=" + ts : ""); } - // If data is available, append data to url for get requests - if ( s.data && type === "GET" ) { + // If data is available, append data to url for GET/HEAD requests + if ( s.data && noContent ) { s.url += (rquery.test(s.url) ? "&" : "?") + s.data; } @@ -5584,7 +5852,7 @@ jQuery.extend({ // Matches an absolute URL, and saves the domain var parts = rurl.exec( s.url ), - remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host); + remote = parts && (parts[1] && parts[1].toLowerCase() !== location.protocol || parts[2].toLowerCase() !== location.host); // If we're requesting a remote document // and trying to load JSON or Script with a GET @@ -5760,10 +6028,11 @@ jQuery.extend({ try { var oldAbort = xhr.abort; xhr.abort = function() { - // xhr.abort in IE7 is not a native JS function - // and does not have a call property - if ( xhr && oldAbort.call ) { - oldAbort.call( xhr ); + if ( xhr ) { + // oldAbort has no call property in IE7 so + // just do it this way, which works in all + // browsers + Function.prototype.call.call( oldAbort, xhr ); } onreadystatechange( "abort" ); @@ -5803,11 +6072,12 @@ jQuery.extend({ // Serialize an array of form elements or a set of // key/values into a query string param: function( a, traditional ) { - var s = [], add = function( key, value ) { - // If value is a function, invoke it and return its value - value = jQuery.isFunction(value) ? value() : value; - s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value); - }; + var s = [], + add = function( key, value ) { + // If value is a function, invoke it and return its value + value = jQuery.isFunction(value) ? value() : value; + s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value); + }; // Set traditional to true for jQuery <= 1.3.2 behavior. if ( traditional === undefined ) { @@ -6029,28 +6299,39 @@ var elemdisplay = {}, jQuery.fn.extend({ show: function( speed, easing, callback ) { + var elem, display; + if ( speed || speed === 0 ) { return this.animate( genFx("show", 3), speed, easing, callback); + } else { for ( var i = 0, j = this.length; i < j; i++ ) { + elem = this[i]; + display = elem.style.display; + // Reset the inline display of this element to learn if it is // being hidden by cascaded rules or not - if ( !jQuery.data(this[i], "olddisplay") && this[i].style.display === "none" ) { - this[i].style.display = ""; + if ( !jQuery.data(elem, "olddisplay") && display === "none" ) { + display = elem.style.display = ""; } // Set elements which have been overridden with display: none // in a stylesheet to whatever the default browser style is // for such an element - if ( this[i].style.display === "" && jQuery.css( this[i], "display" ) === "none" ) { - jQuery.data(this[i], "olddisplay", defaultDisplay(this[i].nodeName)); + if ( display === "" && jQuery.css( elem, "display" ) === "none" ) { + jQuery.data(elem, "olddisplay", defaultDisplay(elem.nodeName)); } } // Set the display of most of the elements in a second loop // to avoid the constant reflow for ( i = 0; i < j; i++ ) { - this[i].style.display = jQuery.data(this[i], "olddisplay") || ""; + elem = this[i]; + display = elem.style.display; + + if ( display === "" || display === "none" ) { + elem.style.display = jQuery.data(elem, "olddisplay") || ""; + } } return this; @@ -6115,7 +6396,7 @@ jQuery.fn.extend({ } return this[ optall.queue === false ? "each" : "queue" ](function() { - // XXX ‘this’ does not always have a nodeName when running the + // XXX 'this' does not always have a nodeName when running the // test suite var opt = jQuery.extend({}, optall), p, @@ -6188,7 +6469,7 @@ jQuery.fn.extend({ } else { var parts = rfxnum.exec(val), - start = e.cur(true) || 0; + start = e.cur() || 0; if ( parts ) { var end = parseFloat( parts[2] ), @@ -6197,7 +6478,7 @@ jQuery.fn.extend({ // We need to compute starting value if ( unit !== "px" ) { jQuery.style( self, name, (end || 1) + unit); - start = ((end || 1) / e.cur(true)) * start; + start = ((end || 1) / e.cur()) * start; jQuery.style( self, name, start + unit); } @@ -6266,7 +6547,8 @@ jQuery.each({ slideUp: genFx("hide", 1), slideToggle: genFx("toggle", 1), fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" } + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } }, function( name, props ) { jQuery.fn[ name ] = function( speed, easing, callback ) { return this.animate( props, speed, easing, callback ); @@ -6344,6 +6626,9 @@ jQuery.fx.prototype = { // Start an animation from one number to another custom: function( from, to, unit ) { + var self = this, + fx = jQuery.fx; + this.startTime = jQuery.now(); this.start = from; this.end = to; @@ -6351,7 +6636,6 @@ jQuery.fx.prototype = { this.now = this.start; this.pos = this.state = 0; - var self = this, fx = jQuery.fx; function t( gotoEnd ) { return self.step(gotoEnd); } @@ -6408,7 +6692,9 @@ jQuery.fx.prototype = { if ( done ) { // Reset the overflow if ( this.options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { - var elem = this.elem, options = this.options; + var elem = this.elem, + options = this.options; + jQuery.each( [ "", "X", "Y" ], function (index, value) { elem.style[ "overflow" + value ] = options.overflow[index]; } ); @@ -6587,11 +6873,16 @@ if ( "getBoundingClientRect" in document.documentElement ) { jQuery.offset.initialize(); - var offsetParent = elem.offsetParent, prevOffsetParent = elem, - doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement, - body = doc.body, defaultView = doc.defaultView, + var computedStyle, + offsetParent = elem.offsetParent, + prevOffsetParent = elem, + doc = elem.ownerDocument, + docElem = doc.documentElement, + body = doc.body, + defaultView = doc.defaultView, prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle, - top = elem.offsetTop, left = elem.offsetLeft; + top = elem.offsetTop, + left = elem.offsetLeft; while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) { if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { @@ -6673,7 +6964,8 @@ jQuery.offset = { }, bodyOffset: function( body ) { - var top = body.offsetTop, left = body.offsetLeft; + var top = body.offsetTop, + left = body.offsetLeft; jQuery.offset.initialize(); @@ -6854,27 +7146,31 @@ jQuery.each([ "Height", "Width" ], function( i, name ) { }); } - return jQuery.isWindow( elem ) ? + if ( jQuery.isWindow( elem ) ) { // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode - elem.document.compatMode === "CSS1Compat" && elem.document.documentElement[ "client" + name ] || - elem.document.body[ "client" + name ] : - - // Get document width or height - (elem.nodeType === 9) ? // is it a document - // Either scroll[Width/Height] or offset[Width/Height], whichever is greater - Math.max( - elem.documentElement["client" + name], - elem.body["scroll" + name], elem.documentElement["scroll" + name], - elem.body["offset" + name], elem.documentElement["offset" + name] - ) : - - // Get or set width or height on the element - size === undefined ? - // Get width or height on the element - parseFloat( jQuery.css( elem, type ) ) : - - // Set the width or height on the element (default to pixels if value is unitless) - this.css( type, typeof size === "string" ? size : size + "px" ); + return elem.document.compatMode === "CSS1Compat" && elem.document.documentElement[ "client" + name ] || + elem.document.body[ "client" + name ]; + + // Get document width or height + } else if ( elem.nodeType === 9 ) { + // Either scroll[Width/Height] or offset[Width/Height], whichever is greater + return Math.max( + elem.documentElement["client" + name], + elem.body["scroll" + name], elem.documentElement["scroll" + name], + elem.body["offset" + name], elem.documentElement["offset" + name] + ); + + // Get or set width or height on the element + } else if ( size === undefined ) { + var orig = jQuery.css( elem, type ), + ret = parseFloat( orig ); + + return jQuery.isNaN( ret ) ? orig : ret; + + // Set the width or height on the element (default to pixels if value is unitless) + } else { + return this.css( type, typeof size === "string" ? size : size + "px" ); + } }; }); diff --git a/railties/lib/rails/generators/rails/app/templates/public/javascripts/prototype.js b/railties/lib/rails/generators/rails/app/templates/public/javascripts/prototype.js index 06249a6ae3..474b2231bb 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/javascripts/prototype.js +++ b/railties/lib/rails/generators/rails/app/templates/public/javascripts/prototype.js @@ -1,4 +1,4 @@ -/* Prototype JavaScript framework, version 1.7_rc2 +/* Prototype JavaScript framework, version 1.7 * (c) 2005-2010 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. @@ -8,7 +8,7 @@ var Prototype = { - Version: '1.7_rc2', + Version: '1.7', Browser: (function(){ var ua = navigator.userAgent; @@ -166,10 +166,12 @@ var Class = (function() { NUMBER_TYPE = 'Number', STRING_TYPE = 'String', OBJECT_TYPE = 'Object', + FUNCTION_CLASS = '[object Function]', BOOLEAN_CLASS = '[object Boolean]', NUMBER_CLASS = '[object Number]', STRING_CLASS = '[object String]', ARRAY_CLASS = '[object Array]', + DATE_CLASS = '[object Date]', NATIVE_JSON_STRINGIFY_SUPPORT = window.JSON && typeof JSON.stringify === 'function' && JSON.stringify(0) === '0' && @@ -322,7 +324,7 @@ var Class = (function() { } function isFunction(object) { - return typeof object === "function"; + return _toString.call(object) === FUNCTION_CLASS; } function isString(object) { @@ -333,6 +335,10 @@ var Class = (function() { return _toString.call(object) === NUMBER_CLASS; } + function isDate(object) { + return _toString.call(object) === DATE_CLASS; + } + function isUndefined(object) { return typeof object === "undefined"; } @@ -352,6 +358,7 @@ var Class = (function() { isFunction: isFunction, isString: isString, isNumber: isNumber, + isDate: isDate, isUndefined: isUndefined }); })(); @@ -1079,9 +1086,10 @@ Array.from = $A; slice = arrayProto.slice, _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available - function each(iterator) { - for (var i = 0, length = this.length; i < length; i++) - iterator(this[i]); + function each(iterator, context) { + for (var i = 0, length = this.length >>> 0; i < length; i++) { + if (i in this) iterator.call(context, this[i], i, this); + } } if (!_each) _each = each; @@ -1287,8 +1295,14 @@ var Hash = Class.create(Enumerable, (function() { var key = encodeURIComponent(pair.key), values = pair.value; if (values && typeof values == 'object') { - if (Object.isArray(values)) - return results.concat(values.map(toQueryPair.curry(key))); + if (Object.isArray(values)) { + var queryValues = []; + for (var i = 0, len = values.length, value; i < len; i++) { + value = values[i]; + queryValues.push(toQueryPair(key, value)); + } + return results.concat(queryValues); + } } else results.push(toQueryPair(key, values)); return results; }).join('&'); @@ -1468,9 +1482,7 @@ Ajax.Base = Class.create({ this.options.method = this.options.method.toLowerCase(); - if (Object.isString(this.options.parameters)) - this.options.parameters = this.options.parameters.toQueryParams(); - else if (Object.isHash(this.options.parameters)) + if (Object.isHash(this.options.parameters)) this.options.parameters = this.options.parameters.toObject(); } }); @@ -1486,22 +1498,21 @@ Ajax.Request = Class.create(Ajax.Base, { request: function(url) { this.url = url; this.method = this.options.method; - var params = Object.clone(this.options.parameters); + var params = Object.isString(this.options.parameters) ? + this.options.parameters : + Object.toQueryString(this.options.parameters); if (!['get', 'post'].include(this.method)) { - params['_method'] = this.method; + params += (params ? '&' : '') + "_method=" + this.method; this.method = 'post'; } - this.parameters = params; - - if (params = Object.toQueryString(params)) { - if (this.method == 'get') - this.url += (this.url.include('?') ? '&' : '?') + params; - else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) - params += '&_='; + if (params && this.method === 'get') { + this.url += (this.url.include('?') ? '&' : '?') + params; } + this.parameters = params.toQueryParams(); + try { var response = new Ajax.Response(this); if (this.options.onCreate) this.options.onCreate(response); @@ -1570,11 +1581,12 @@ Ajax.Request = Class.create(Ajax.Base, { success: function() { var status = this.getStatus(); - return !status || (status >= 200 && status < 300); + return !status || (status >= 200 && status < 300) || status == 304; }, getStatus: function() { try { + if (this.transport.status === 1223) return 204; return this.transport.status || 0; } catch (e) { return 0 } }, @@ -1849,6 +1861,11 @@ if (!Node.ELEMENT_NODE) { (function(global) { + function shouldUseCache(tagName, attributes) { + if (tagName === 'select') return false; + if ('type' in attributes) return false; + return true; + } var HAS_EXTENDED_CREATE_ELEMENT_SYNTAX = (function(){ try { @@ -1866,13 +1883,19 @@ if (!Node.ELEMENT_NODE) { attributes = attributes || { }; tagName = tagName.toLowerCase(); var cache = Element.cache; + if (HAS_EXTENDED_CREATE_ELEMENT_SYNTAX && attributes.name) { tagName = '<' + tagName + ' name="' + attributes.name + '">'; delete attributes.name; return Element.writeAttribute(document.createElement(tagName), attributes); } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); - return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + + var node = shouldUseCache(tagName, attributes) ? + cache[tagName].cloneNode(false) : document.createElement(tagName); + + return Element.writeAttribute(node, attributes); }; Object.extend(global.Element, element || { }); @@ -1883,7 +1906,7 @@ if (!Node.ELEMENT_NODE) { Element.idCounter = 1; Element.cache = { }; -function purgeElement(element) { +Element._purgeElement = function(element) { var uid = element._prototypeUID; if (uid) { Element.stopObserving(element); @@ -1948,6 +1971,21 @@ Element.Methods = { } })(); + var LINK_ELEMENT_INNERHTML_BUGGY = (function() { + try { + var el = document.createElement('div'); + el.innerHTML = "<link>"; + var isBuggy = (el.childNodes.length === 0); + el = null; + return isBuggy; + } catch(e) { + return true; + } + })(); + + var ANY_INNERHTML_BUGGY = SELECT_ELEMENT_INNERHTML_BUGGY || + TABLE_ELEMENT_INNERHTML_BUGGY || LINK_ELEMENT_INNERHTML_BUGGY; + var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () { var s = document.createElement("script"), isBuggy = false; @@ -1962,8 +2000,10 @@ Element.Methods = { return isBuggy; })(); + function update(element, content) { element = $(element); + var purgeElement = Element._purgeElement; var descendants = element.getElementsByTagName('*'), i = descendants.length; @@ -1984,7 +2024,7 @@ Element.Methods = { return element; } - if (SELECT_ELEMENT_INNERHTML_BUGGY || TABLE_ELEMENT_INNERHTML_BUGGY) { + if (ANY_INNERHTML_BUGGY) { if (tagName in Element._insertionTranslations.tags) { while (element.firstChild) { element.removeChild(element.firstChild); @@ -1993,6 +2033,12 @@ Element.Methods = { .each(function(node) { element.appendChild(node) }); + } else if (LINK_ELEMENT_INNERHTML_BUGGY && Object.isString(content) && content.indexOf('<link') > -1) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + var nodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts(), true); + nodes.each(function(node) { element.appendChild(node) }); } else { element.innerHTML = content.stripScripts(); @@ -2402,117 +2448,6 @@ Element.Methods = { return element; }, - cumulativeOffset: function(element) { - var valueT = 0, valueL = 0; - if (element.parentNode) { - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - } while (element); - } - return Element._returnOffset(valueL, valueT); - }, - - positionedOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - if (element) { - if (element.tagName.toUpperCase() == 'BODY') break; - var p = Element.getStyle(element, 'position'); - if (p !== 'static') break; - } - } while (element); - return Element._returnOffset(valueL, valueT); - }, - - absolutize: function(element) { - element = $(element); - if (Element.getStyle(element, 'position') == 'absolute') return element; - - var offsets = Element.positionedOffset(element), - top = offsets[1], - left = offsets[0], - width = element.clientWidth, - height = element.clientHeight; - - element._originalLeft = left - parseFloat(element.style.left || 0); - element._originalTop = top - parseFloat(element.style.top || 0); - element._originalWidth = element.style.width; - element._originalHeight = element.style.height; - - element.style.position = 'absolute'; - element.style.top = top + 'px'; - element.style.left = left + 'px'; - element.style.width = width + 'px'; - element.style.height = height + 'px'; - return element; - }, - - relativize: function(element) { - element = $(element); - if (Element.getStyle(element, 'position') == 'relative') return element; - - element.style.position = 'relative'; - var top = parseFloat(element.style.top || 0) - (element._originalTop || 0), - left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); - - element.style.top = top + 'px'; - element.style.left = left + 'px'; - element.style.height = element._originalHeight; - element.style.width = element._originalWidth; - return element; - }, - - cumulativeScrollOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.scrollTop || 0; - valueL += element.scrollLeft || 0; - element = element.parentNode; - } while (element); - return Element._returnOffset(valueL, valueT); - }, - - getOffsetParent: function(element) { - if (element.offsetParent) return $(element.offsetParent); - if (element == document.body) return $(element); - - while ((element = element.parentNode) && element != document.body) - if (Element.getStyle(element, 'position') != 'static') - return $(element); - - return $(document.body); - }, - - viewportOffset: function(forElement) { - var valueT = 0, - valueL = 0, - element = forElement; - - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - - if (element.offsetParent == document.body && - Element.getStyle(element, 'position') == 'absolute') break; - - } while (element = element.offsetParent); - - element = forElement; - do { - if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { - valueT -= element.scrollTop || 0; - valueL -= element.scrollLeft || 0; - } - } while (element = element.parentNode); - - return Element._returnOffset(valueL, valueT); - }, - clonePosition: function(element, source) { var options = Object.extend({ setLeft: true, @@ -2566,8 +2501,6 @@ if (Prototype.Browser.Opera) { Element.Methods.getStyle = Element.Methods.getStyle.wrap( function(proceed, element, style) { switch (style) { - case 'left': case 'top': case 'right': case 'bottom': - if (proceed(element, 'position') === 'static') return null; case 'height': case 'width': if (!Element.visible(element)) return null; @@ -2603,37 +2536,6 @@ if (Prototype.Browser.Opera) { } else if (Prototype.Browser.IE) { - Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( - function(proceed, element) { - element = $(element); - if (!element.parentNode) return $(document.body); - var position = element.getStyle('position'); - if (position !== 'static') return proceed(element); - element.setStyle({ position: 'relative' }); - var value = proceed(element); - element.setStyle({ position: position }); - return value; - } - ); - - $w('positionedOffset viewportOffset').each(function(method) { - Element.Methods[method] = Element.Methods[method].wrap( - function(proceed, element) { - element = $(element); - if (!element.parentNode) return Element._returnOffset(0, 0); - var position = element.getStyle('position'); - if (position !== 'static') return proceed(element); - var offsetParent = element.getOffsetParent(); - if (offsetParent && offsetParent.getStyle('position') === 'fixed') - offsetParent.setStyle({ zoom: 1 }); - element.setStyle({ position: 'relative' }); - var value = proceed(element); - element.setStyle({ position: position }); - return value; - } - ); - }); - Element.Methods.getStyle = function(element, style) { element = $(element); style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); @@ -2862,20 +2764,6 @@ else if (Prototype.Browser.WebKit) { return element; }; - - Element.Methods.cumulativeOffset = function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - if (element.offsetParent == document.body) - if (Element.getStyle(element, 'position') == 'absolute') break; - - element = element.offsetParent; - } while (element); - - return Element._returnOffset(valueL, valueT); - }; } if ('outerHTML' in document.documentElement) { @@ -2914,11 +2802,20 @@ Element._returnOffset = function(l, t) { return result; }; -Element._getContentFromAnonymousElement = function(tagName, html) { +Element._getContentFromAnonymousElement = function(tagName, html, force) { var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; - if (t) { - div.innerHTML = t[0] + html + t[1]; + + var workaround = false; + if (t) workaround = true; + else if (force) { + workaround = true; + t = ['', '', 0]; + } + + if (workaround) { + div.innerHTML = ' ' + t[0] + html + t[1]; + div.removeChild(div.firstChild); for (var i = t[2]; i--; ) { div = div.firstChild; } @@ -3077,7 +2974,8 @@ Element.addMethods = function(methods) { "FORM": Object.clone(Form.Methods), "INPUT": Object.clone(Form.Element.Methods), "SELECT": Object.clone(Form.Element.Methods), - "TEXTAREA": Object.clone(Form.Element.Methods) + "TEXTAREA": Object.clone(Form.Element.Methods), + "BUTTON": Object.clone(Form.Element.Methods) }); } @@ -3264,6 +3162,8 @@ Element.addMethods({ purge: function(element) { if (!(element = $(element))) return; + var purgeElement = Element._purgeElement; + purgeElement(element); var descendants = element.getElementsByTagName('*'), @@ -3283,11 +3183,13 @@ Element.addMethods({ return (Number(match[1]) / 100); } - function getPixelValue(value, property) { + function getPixelValue(value, property, context) { + var element = null; if (Object.isElement(value)) { element = value; value = element.getStyle(property); } + if (value === null) { return null; } @@ -3296,7 +3198,9 @@ Element.addMethods({ return window.parseFloat(value); } - if (/\d/.test(value) && element.runtimeStyle) { + var isPercentage = value.include('%'), isViewport = (context === document.viewport); + + if (/\d/.test(value) && element && element.runtimeStyle && !(isPercentage && isViewport)) { var style = element.style.left, rStyle = element.runtimeStyle.left; element.runtimeStyle.left = element.currentStyle.left; element.style.left = value || 0; @@ -3307,18 +3211,33 @@ Element.addMethods({ return value; } - if (value.include('%')) { + if (element && isPercentage) { + context = context || element.parentNode; var decimal = toDecimal(value); - var whole; - if (property.include('left') || property.include('right') || - property.include('width')) { - whole = $(element.parentNode).measure('width'); - } else if (property.include('top') || property.include('bottom') || - property.include('height')) { - whole = $(element.parentNode).measure('height'); + var whole = null; + var position = element.getStyle('position'); + + var isHorizontal = property.include('left') || property.include('right') || + property.include('width'); + + var isVertical = property.include('top') || property.include('bottom') || + property.include('height'); + + if (context === document.viewport) { + if (isHorizontal) { + whole = document.viewport.getWidth(); + } else if (isVertical) { + whole = document.viewport.getHeight(); + } + } else { + if (isHorizontal) { + whole = $(context).measure('width'); + } else if (isVertical) { + whole = $(context).measure('height'); + } } - return whole * decimal; + return (whole === null) ? 0 : whole * decimal; } return 0; @@ -3410,6 +3329,14 @@ Element.addMethods({ var position = element.getStyle('position'), width = element.getStyle('width'); + if (width === "0px" || width === null) { + element.style.display = 'block'; + width = element.getStyle('width'); + } + + var context = (position === 'fixed') ? document.viewport : + element.parentNode; + element.setStyle({ position: 'absolute', visibility: 'hidden', @@ -3420,9 +3347,9 @@ Element.addMethods({ var newWidth; if (width && (positionedWidth === width)) { - newWidth = getPixelValue(width); - } else if (width && (position === 'absolute' || position === 'fixed')) { - newWidth = getPixelValue(width); + newWidth = getPixelValue(element, 'width', context); + } else if (position === 'absolute' || position === 'fixed') { + newWidth = getPixelValue(element, 'width', context); } else { var parent = element.parentNode, pLayout = $(parent).getLayout(); @@ -3453,6 +3380,7 @@ Element.addMethods({ if (!(property in COMPUTATIONS)) { throw "Property not found."; } + return this._set(property, COMPUTATIONS[property].call(this, this.element)); }, @@ -3505,7 +3433,10 @@ Element.addMethods({ if (!this._preComputing) this._begin(); var bHeight = this.get('border-box-height'); - if (bHeight <= 0) return 0; + if (bHeight <= 0) { + if (!this._preComputing) this._end(); + return 0; + } var bTop = this.get('border-top'), bBottom = this.get('border-bottom'); @@ -3522,7 +3453,10 @@ Element.addMethods({ if (!this._preComputing) this._begin(); var bWidth = this.get('border-box-width'); - if (bWidth <= 0) return 0; + if (bWidth <= 0) { + if (!this._preComputing) this._end(); + return 0; + } var bLeft = this.get('border-left'), bRight = this.get('border-right'); @@ -3552,11 +3486,17 @@ Element.addMethods({ }, 'border-box-height': function(element) { - return element.offsetHeight; + if (!this._preComputing) this._begin(); + var height = element.offsetHeight; + if (!this._preComputing) this._end(); + return height; }, 'border-box-width': function(element) { - return element.offsetWidth; + if (!this._preComputing) this._begin(); + var width = element.offsetWidth; + if (!this._preComputing) this._end(); + return width; }, 'margin-box-height': function(element) { @@ -3626,23 +3566,19 @@ Element.addMethods({ }, 'border-top': function(element) { - return Object.isNumber(element.clientTop) ? element.clientTop : - getPixelValue(element, 'borderTopWidth'); + return getPixelValue(element, 'borderTopWidth'); }, 'border-bottom': function(element) { - return Object.isNumber(element.clientBottom) ? element.clientBottom : - getPixelValue(element, 'borderBottomWidth'); + return getPixelValue(element, 'borderBottomWidth'); }, 'border-left': function(element) { - return Object.isNumber(element.clientLeft) ? element.clientLeft : - getPixelValue(element, 'borderLeftWidth'); + return getPixelValue(element, 'borderLeftWidth'); }, 'border-right': function(element) { - return Object.isNumber(element.clientRight) ? element.clientRight : - getPixelValue(element, 'borderRightWidth'); + return getPixelValue(element, 'borderRightWidth'); }, 'margin-top': function(element) { @@ -3721,23 +3657,52 @@ Element.addMethods({ } function getDimensions(element) { - var layout = $(element).getLayout(); - return { - width: layout.get('width'), - height: layout.get('height') + element = $(element); + var display = Element.getStyle(element, 'display'); + + if (display && display !== 'none') { + return { width: element.offsetWidth, height: element.offsetHeight }; + } + + var style = element.style; + var originalStyles = { + visibility: style.visibility, + position: style.position, + display: style.display + }; + + var newStyles = { + visibility: 'hidden', + display: 'block' }; + + if (originalStyles.position !== 'fixed') + newStyles.position = 'absolute'; + + Element.setStyle(element, newStyles); + + var dimensions = { + width: element.offsetWidth, + height: element.offsetHeight + }; + + Element.setStyle(element, originalStyles); + + return dimensions; } function getOffsetParent(element) { - if (isDetached(element)) return $(document.body); + element = $(element); + + if (isDocument(element) || isDetached(element) || isBody(element) || isHtml(element)) + return $(document.body); var isInline = (Element.getStyle(element, 'display') === 'inline'); if (!isInline && element.offsetParent) return $(element.offsetParent); - if (element === document.body) return $(element); while ((element = element.parentNode) && element !== document.body) { if (Element.getStyle(element, 'position') !== 'static') { - return (element.nodeName === 'HTML') ? $(document.body) : $(element); + return isHtml(element) ? $(document.body) : $(element); } } @@ -3746,16 +3711,21 @@ Element.addMethods({ function cumulativeOffset(element) { + element = $(element); var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - } while (element); + if (element.parentNode) { + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + } return new Element.Offset(valueL, valueT); } function positionedOffset(element) { + element = $(element); + var layout = element.getLayout(); var valueT = 0, valueL = 0; @@ -3787,6 +3757,7 @@ Element.addMethods({ } function viewportOffset(forElement) { + element = $(element); var valueT = 0, valueL = 0, docBody = document.body; var element = forElement; @@ -3852,6 +3823,57 @@ Element.addMethods({ return element; } + if (Prototype.Browser.IE) { + getOffsetParent = getOffsetParent.wrap( + function(proceed, element) { + element = $(element); + + if (isDocument(element) || isDetached(element) || isBody(element) || isHtml(element)) + return $(document.body); + + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + + positionedOffset = positionedOffset.wrap(function(proceed, element) { + element = $(element); + if (!element.parentNode) return new Element.Offset(0, 0); + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + + var offsetParent = element.getOffsetParent(); + if (offsetParent && offsetParent.getStyle('position') === 'fixed') + hasLayout(offsetParent); + + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + }); + } else if (Prototype.Browser.Webkit) { + cumulativeOffset = function(element) { + element = $(element); + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return new Element.Offset(valueL, valueT); + }; + } + + Element.addMethods({ getLayout: getLayout, measure: measure, @@ -3869,6 +3891,14 @@ Element.addMethods({ return element.nodeName.toUpperCase() === 'BODY'; } + function isHtml(element) { + return element.nodeName.toUpperCase() === 'HTML'; + } + + function isDocument(element) { + return element.nodeType === Node.DOCUMENT_NODE; + } + function isDetached(element) { return element !== document.body && !Element.descendantOf(element, document.body); @@ -3880,32 +3910,10 @@ Element.addMethods({ element = $(element); if (isDetached(element)) return new Element.Offset(0, 0); - var rect = element.getBoundingClientRect(), + var rect = element.getBoundingClientRect(), docEl = document.documentElement; return new Element.Offset(rect.left - docEl.clientLeft, rect.top - docEl.clientTop); - }, - - positionedOffset: function(element) { - element = $(element); - var parent = element.getOffsetParent(); - if (isDetached(element)) return new Element.Offset(0, 0); - - if (element.offsetParent && - element.offsetParent.nodeName.toUpperCase() === 'HTML') { - return positionedOffset(element); - } - - var eOffset = element.viewportOffset(), - pOffset = isBody(parent) ? viewportOffset(parent) : - parent.viewportOffset(); - var retOffset = eOffset.relativeTo(pOffset); - - var layout = element.getLayout(); - var top = retOffset.top - layout.get('margin-top'); - var left = retOffset.left - layout.get('margin-left'); - - return new Element.Offset(left, top); } }); } @@ -4962,24 +4970,34 @@ var Form = { serializeElements: function(elements, options) { if (typeof options != 'object') options = { hash: !!options }; else if (Object.isUndefined(options.hash)) options.hash = true; - var key, value, submitted = false, submit = options.submit; + var key, value, submitted = false, submit = options.submit, accumulator, initial; + + if (options.hash) { + initial = {}; + accumulator = function(result, key, value) { + if (key in result) { + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } else result[key] = value; + return result; + }; + } else { + initial = ''; + accumulator = function(result, key, value) { + return result + (result ? '&' : '') + encodeURIComponent(key) + '=' + encodeURIComponent(value); + } + } - var data = elements.inject({ }, function(result, element) { + return elements.inject(initial, function(result, element) { if (!element.disabled && element.name) { key = element.name; value = $(element).getValue(); if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && submit !== false && (!submit || key == submit) && (submitted = true)))) { - if (key in result) { - if (!Object.isArray(result[key])) result[key] = [result[key]]; - result[key].push(value); - } - else result[key] = value; + result = accumulator(result, key, value); } } return result; }); - - return options.hash ? data : Object.toQueryString(data); } }; @@ -5046,7 +5064,8 @@ Form.Methods = { focusFirstElement: function(form) { form = $(form); - form.findFirstElement().activate(); + var element = form.findFirstElement(); + if (element) element.activate(); return form; }, @@ -5153,67 +5172,77 @@ var $F = Form.Element.Methods.getValue; /*--------------------------------------------------------------------------*/ -Form.Element.Serializers = { - input: function(element, value) { +Form.Element.Serializers = (function() { + function input(element, value) { switch (element.type.toLowerCase()) { case 'checkbox': case 'radio': - return Form.Element.Serializers.inputSelector(element, value); + return inputSelector(element, value); default: - return Form.Element.Serializers.textarea(element, value); + return valueSelector(element, value); } - }, + } - inputSelector: function(element, value) { - if (Object.isUndefined(value)) return element.checked ? element.value : null; + function inputSelector(element, value) { + if (Object.isUndefined(value)) + return element.checked ? element.value : null; else element.checked = !!value; - }, + } - textarea: function(element, value) { + function valueSelector(element, value) { if (Object.isUndefined(value)) return element.value; else element.value = value; - }, + } - select: function(element, value) { + function select(element, value) { if (Object.isUndefined(value)) - return this[element.type == 'select-one' ? - 'selectOne' : 'selectMany'](element); - else { - var opt, currentValue, single = !Object.isArray(value); - for (var i = 0, length = element.length; i < length; i++) { - opt = element.options[i]; - currentValue = this.optionValue(opt); - if (single) { - if (currentValue == value) { - opt.selected = true; - return; - } + return (element.type === 'select-one' ? selectOne : selectMany)(element); + + var opt, currentValue, single = !Object.isArray(value); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + currentValue = this.optionValue(opt); + if (single) { + if (currentValue == value) { + opt.selected = true; + return; } - else opt.selected = value.include(currentValue); } + else opt.selected = value.include(currentValue); } - }, + } - selectOne: function(element) { + function selectOne(element) { var index = element.selectedIndex; - return index >= 0 ? this.optionValue(element.options[index]) : null; - }, + return index >= 0 ? optionValue(element.options[index]) : null; + } - selectMany: function(element) { + function selectMany(element) { var values, length = element.length; if (!length) return null; for (var i = 0, values = []; i < length; i++) { var opt = element.options[i]; - if (opt.selected) values.push(this.optionValue(opt)); + if (opt.selected) values.push(optionValue(opt)); } return values; - }, + } - optionValue: function(opt) { - return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + function optionValue(opt) { + return Element.hasAttribute(opt, 'value') ? opt.value : opt.text; } -}; + + return { + input: input, + inputSelector: inputSelector, + textarea: valueSelector, + select: select, + selectOne: selectOne, + selectMany: selectMany, + optionValue: optionValue, + button: valueSelector + }; +})(); /*--------------------------------------------------------------------------*/ @@ -5324,24 +5353,53 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl && 'onmouseleave' in docEl; + + + var isIELegacyEvent = function(event) { return false; }; + + if (window.attachEvent) { + if (window.addEventListener) { + isIELegacyEvent = function(event) { + return !(event instanceof window.Event); + }; + } else { + isIELegacyEvent = function(event) { return true; }; + } + } + var _isButton; - if (Prototype.Browser.IE) { - var buttonMap = { 0: 1, 1: 4, 2: 2 }; - _isButton = function(event, code) { - return event.button === buttonMap[code]; - }; - } else if (Prototype.Browser.WebKit) { - _isButton = function(event, code) { - switch (code) { - case 0: return event.which == 1 && !event.metaKey; - case 1: return event.which == 1 && event.metaKey; - default: return false; + + function _isButtonForDOMEvents(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + } + + var legacyButtonMap = { 0: 1, 1: 4, 2: 2 }; + function _isButtonForLegacyEvents(event, code) { + return event.button === legacyButtonMap[code]; + } + + function _isButtonForWebKit(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 2 || (event.which == 1 && event.metaKey); + case 2: return event.which == 3; + default: return false; + } + } + + if (window.attachEvent) { + if (!window.addEventListener) { + _isButton = _isButtonForLegacyEvents; + } else { + _isButton = function(event, code) { + return isIELegacyEvent(event) ? _isButtonForLegacyEvents(event, code) : + _isButtonForDOMEvents(event, code); } - }; + } + } else if (Prototype.Browser.WebKit) { + _isButton = _isButtonForWebKit; } else { - _isButton = function(event, code) { - return event.which ? (event.which === code + 1) : (event.button === code); - }; + _isButton = _isButtonForDOMEvents; } function isLeftClick(event) { return _isButton(event, 0) } @@ -5371,6 +5429,7 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { function findElement(event, expression) { var element = Event.element(event); + if (!expression) return element; while (element) { if (Object.isElement(element) && Prototype.Selector.match(element, expression)) { @@ -5411,49 +5470,59 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { event.stopped = true; } + Event.Methods = { - isLeftClick: isLeftClick, + isLeftClick: isLeftClick, isMiddleClick: isMiddleClick, - isRightClick: isRightClick, + isRightClick: isRightClick, - element: element, + element: element, findElement: findElement, - pointer: pointer, + pointer: pointer, pointerX: pointerX, pointerY: pointerY, stop: stop }; - var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { m[name] = Event.Methods[name].methodize(); return m; }); - if (Prototype.Browser.IE) { + if (window.attachEvent) { function _relatedTarget(event) { var element; switch (event.type) { - case 'mouseover': element = event.fromElement; break; - case 'mouseout': element = event.toElement; break; - default: return null; + case 'mouseover': + case 'mouseenter': + element = event.fromElement; + break; + case 'mouseout': + case 'mouseleave': + element = event.toElement; + break; + default: + return null; } return Element.extend(element); } - Object.extend(methods, { + var additionalMethods = { stopPropagation: function() { this.cancelBubble = true }, preventDefault: function() { this.returnValue = false }, inspect: function() { return '[object Event]' } - }); + }; Event.extend = function(event, element) { if (!event) return false; - if (event._extendedByPrototype) return event; + if (!isIELegacyEvent(event)) return event; + + if (event._extendedByPrototype) return event; event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); Object.extend(event, { @@ -5463,12 +5532,18 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { pageY: pointer.y }); - return Object.extend(event, methods); + Object.extend(event, methods); + Object.extend(event, additionalMethods); + + return event; }; } else { + Event.extend = Prototype.K; + } + + if (window.addEventListener) { Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__; Object.extend(Event.prototype, methods); - Event.extend = Prototype.K; } function _createResponder(element, eventName, handler) { @@ -5567,7 +5642,7 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { element.addEventListener("dataavailable", responder, false); else { element.attachEvent("ondataavailable", responder); - element.attachEvent("onfilterchange", responder); + element.attachEvent("onlosecapture", responder); } } else { var actualEventName = _getDOMEventName(eventName); @@ -5605,7 +5680,13 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { return element; } - var responder = responders.find( function(r) { return r.handler === handler; }); + var i = responders.length, responder; + while (i--) { + if (responders[i].handler === handler) { + responder = responders[i]; + break; + } + } if (!responder) return element; if (eventName.include(':')) { @@ -5613,7 +5694,7 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { element.removeEventListener("dataavailable", responder, false); else { element.detachEvent("ondataavailable", responder); - element.detachEvent("onfilterchange", responder); + element.detachEvent("onlosecapture", responder); } } else { var actualEventName = _getDOMEventName(eventName); @@ -5640,10 +5721,10 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { var event; if (document.createEvent) { event = document.createEvent('HTMLEvents'); - event.initEvent('dataavailable', true, true); + event.initEvent('dataavailable', bubble, true); } else { event = document.createEventObject(); - event.eventType = bubble ? 'ondataavailable' : 'onfilterchange'; + event.eventType = bubble ? 'ondataavailable' : 'onlosecapture'; } event.eventName = eventName; @@ -5677,7 +5758,7 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { }, handleEvent: function(event) { - var element = event.findElement(this.selector); + var element = Event.findElement(event, this.selector); if (element) this.callback.call(this.element, event, element); } }); diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 40ed2062d3..97f681d826 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -1,3 +1,4 @@ + require 'rails/generators/rails/generator/generator_generator' module Rails @@ -5,6 +6,12 @@ module Rails class PluginGenerator < NamedBase class_option :tasks, :desc => "When supplied creates tasks base files." + def show_deprecation + return unless behavior == :invoke + message = "Plugin generator is deprecated, please use 'rails plugin new' command to generate plugin structure." + ActiveSupport::Deprecation.warn message + end + check_class_collision def create_root_files diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt index 733e321c94..77149ae351 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/rdoctask' diff --git a/railties/lib/rails/generators/rails/plugin_new/USAGE b/railties/lib/rails/generators/rails/plugin_new/USAGE new file mode 100644 index 0000000000..9a7bf9f396 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/USAGE @@ -0,0 +1,10 @@ +Description: + The 'rails plugin new' command creates a skeleton for developing any + kind of Rails extension with ability to run tests using dummy Rails + application. + +Example: + rails plugin new ~/Code/Ruby/blog + + This generates a skeletal Rails plugin in ~/Code/Ruby/blog. + See the README in the newly created plugin to get going. diff --git a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb new file mode 100644 index 0000000000..9c54b98238 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb @@ -0,0 +1,249 @@ +require 'active_support/core_ext/hash/slice' +require "rails/generators/rails/app/app_generator" + +module Rails + class PluginBuilder + def rakefile + template "Rakefile" + end + + def app + directory "app" if options[:mountable] + end + + def readme + copy_file "README.rdoc" + end + + def gemfile + template "Gemfile" + end + + def license + template "MIT-LICENSE" + end + + def gemspec + template "%name%.gemspec" + end + + def gitignore + copy_file "gitignore", ".gitignore" + end + + def lib + template "lib/%name%.rb" + template "lib/tasks/%name%_tasks.rake" + if full? + template "lib/%name%/engine.rb" + end + end + + def config + template "config/routes.rb" if mountable? + end + + def test + template "test/test_helper.rb" + template "test/%name%_test.rb" + append_file "Rakefile", <<-EOF +#{rakefile_test_tasks} + +task :default => :test + EOF + if full? + template "test/integration/navigation_test.rb" + end + end + + def generate_test_dummy(force = false) + opts = (options || {}).slice(:skip_active_record, :skip_javascript, :database, :javascript) + opts[:force] = force + + invoke Rails::Generators::AppGenerator, + [ File.expand_path(dummy_path, destination_root) ], opts + end + + def test_dummy_config + template "rails/boot.rb", "#{dummy_path}/config/boot.rb", :force => true + template "rails/application.rb", "#{dummy_path}/config/application.rb", :force => true + if mountable? + template "rails/routes.rb", "#{dummy_path}/config/routes.rb", :force => true + end + end + + def test_dummy_clean + inside dummy_path do + remove_file ".gitignore" + remove_file "db/seeds.rb" + remove_file "doc" + remove_file "Gemfile" + remove_file "lib/tasks" + remove_file "public/images/rails.png" + remove_file "public/index.html" + remove_file "public/robots.txt" + remove_file "README" + remove_file "test" + remove_file "vendor" + end + end + + def script(force = false) + directory "script", :force => force do |content| + "#{shebang}\n" + content + end + chmod "script", 0755, :verbose => false + end + end + + module Generators + class PluginNewGenerator < AppBase + add_shared_options_for "plugin" + + alias_method :plugin_path, :app_path + + class_option :dummy_path, :type => :string, :default => "test/dummy", + :desc => "Create dummy application at given path" + + class_option :full, :type => :boolean, :default => false, + :desc => "Generate rails engine with integration tests" + + class_option :mountable, :type => :boolean, :default => false, + :desc => "Generate mountable isolated application" + + class_option :skip_gemspec, :type => :boolean, :default => false, + :desc => "Skip gemspec file" + + def initialize(*args) + raise Error, "Options should be given after the plugin name. For details run: rails plugin --help" if args[0].blank? + + super + end + + public_task :create_root + + def create_root_files + build(:readme) + build(:rakefile) + build(:gemspec) unless options[:skip_gemspec] + build(:license) + build(:gitignore) unless options[:skip_git] + build(:gemfile) unless options[:skip_gemfile] + end + + def create_app_files + build(:app) + end + + def create_config_files + build(:config) + end + + def create_lib_files + build(:lib) + end + + def create_script_files + build(:script) + end + + def create_test_files + build(:test) unless options[:skip_test_unit] + end + + def create_test_dummy_files + return if options[:skip_test_unit] + create_dummy_app + end + + def finish_template + build(:leftovers) + end + + public_task :apply_rails_template, :bundle_if_dev_or_edge + + protected + def create_dummy_app(path = nil) + dummy_path(path) if path + + say_status :vendor_app, dummy_path + mute do + build(:generate_test_dummy) + store_application_definition! + build(:test_dummy_config) + build(:test_dummy_clean) + # ensure that script/rails has proper dummy_path + build(:script, true) + end + end + + def full? + options[:full] || options[:mountable] + end + + def mountable? + options[:mountable] + end + + def self.banner + "rails plugin new #{self.arguments.map(&:usage).join(' ')} [options]" + end + + def name + @name ||= File.basename(destination_root) + end + + def camelized + @camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize + end + + def valid_const? + if camelized =~ /^\d/ + raise Error, "Invalid plugin name #{name}. Please give a name which does not start with numbers." + elsif RESERVED_NAMES.include?(name) + raise Error, "Invalid plugin name #{name}. Please give a name which does not match one of the reserved rails words." + elsif Object.const_defined?(camelized) + raise Error, "Invalid plugin name #{name}, constant #{camelized} is already in use. Please choose another application name." + end + end + + def application_definition + @application_definition ||= begin + + dummy_application_path = File.expand_path("#{dummy_path}/config/application.rb", destination_root) + unless options[:pretend] || !File.exists?(dummy_application_path) + contents = File.read(dummy_application_path) + contents[(contents.index("module Dummy"))..-1] + end + end + end + alias :store_application_definition! :application_definition + + def get_builder_class + defined?(::PluginBuilder) ? ::PluginBuilder : Rails::PluginBuilder + end + + def rakefile_test_tasks + <<-RUBY +require 'rake/testtask' + +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = false +end + RUBY + end + + def dummy_path(path = nil) + @dummy_path = path if path + @dummy_path || options[:dummy_path] + end + + def mute(&block) + shell.mute(&block) + end + end + end +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec new file mode 100644 index 0000000000..3d9bfb22c7 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec @@ -0,0 +1,9 @@ +# Provide a simple gemspec so you can easily use your +# project in your rails apps through git. +Gem::Specification.new do |s| + s.name = "<%= name %>" + s.summary = "Insert <%= camelized %> summary." + s.description = "Insert <%= camelized %> description." + s.files = Dir["lib/**/*"] + ["MIT-LICENSE", "Rakefile", "README.rdoc"] + s.version = "0.0.1" +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile b/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile new file mode 100644 index 0000000000..29900c93dc --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile @@ -0,0 +1,11 @@ +source "http://rubygems.org" + +<%= rails_gemfile_entry -%> + +<% if full? -%> +<%= database_gemfile_entry -%> +<% end -%> + +if RUBY_VERSION < '1.9' + gem "ruby-debug", ">= 0.10.3" +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE new file mode 100644 index 0000000000..d7a9109894 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright <%= Date.today.year %> YOURNAME + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/README.rdoc b/railties/lib/rails/generators/rails/plugin_new/templates/README.rdoc new file mode 100644 index 0000000000..301d647731 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/README.rdoc @@ -0,0 +1,3 @@ += <%= camelized %> + +This project rocks and uses MIT-LICENSE.
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile b/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile new file mode 100755 index 0000000000..12350309bf --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile @@ -0,0 +1,16 @@ +#!/usr/bin/env rake +begin + require 'bundler/setup' +rescue LoadError + puts 'You must `gem install bundler` and `bundle install` to run rake tasks' +end + +require 'rake/rdoctask' + +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = '<%= camelized %>' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README.rdoc') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/controllers/%name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin_new/templates/app/controllers/%name%/application_controller.rb.tt new file mode 100644 index 0000000000..448ad7f989 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/app/controllers/%name%/application_controller.rb.tt @@ -0,0 +1,4 @@ +module <%= camelized %> + class ApplicationController < ActionController::Base + end +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/helpers/%name%/application_helper.rb.tt b/railties/lib/rails/generators/rails/plugin_new/templates/app/helpers/%name%/application_helper.rb.tt new file mode 100644 index 0000000000..40ae9f52c2 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/app/helpers/%name%/application_helper.rb.tt @@ -0,0 +1,4 @@ +module <%= camelized %> + module ApplicationHelper + end +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb b/railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb new file mode 100644 index 0000000000..42ddf380d8 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb @@ -0,0 +1,3 @@ +<%= camelized %>::Engine.routes.draw do + +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/gitignore b/railties/lib/rails/generators/rails/plugin_new/templates/gitignore new file mode 100644 index 0000000000..1463de6dfb --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/gitignore @@ -0,0 +1,6 @@ +.bundle/ +log/*.log +pkg/ +test/dummy/db/*.sqlite3 +test/dummy/log/*.log +test/dummy/tmp/
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%.rb b/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%.rb new file mode 100644 index 0000000000..2d3bdc510c --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%.rb @@ -0,0 +1,6 @@ +<% if full? -%> +require "<%= name %>/engine" + +<% end -%> +module <%= camelized %> +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/engine.rb b/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/engine.rb new file mode 100644 index 0000000000..9600ee0c3f --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/engine.rb @@ -0,0 +1,7 @@ +module <%= camelized %> + class Engine < Rails::Engine +<% if mountable? -%> + isolate_namespace <%= camelized %> +<% end -%> + end +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/tasks/%name%_tasks.rake b/railties/lib/rails/generators/rails/plugin_new/templates/lib/tasks/%name%_tasks.rake new file mode 100644 index 0000000000..7121f5ae23 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/lib/tasks/%name%_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :<%= name %> do +# # Task goes here +# end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb new file mode 100644 index 0000000000..8b68280a5e --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb @@ -0,0 +1,16 @@ +require File.expand_path('../boot', __FILE__) + +<% unless options[:skip_active_record] -%> +require 'rails/all' +<% else -%> +# require "active_record/railtie" +require "action_controller/railtie" +require "action_mailer/railtie" +require "active_resource/railtie" +require "rails/test_unit/railtie" +<% end -%> + +Bundler.require +require "<%= name %>" + +<%= application_definition %> diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin_new/templates/rails/boot.rb new file mode 100644 index 0000000000..eba0681370 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/rails/boot.rb @@ -0,0 +1,10 @@ +require 'rubygems' +gemfile = File.expand_path('../../../../Gemfile', __FILE__) + +if File.exist?(gemfile) + ENV['BUNDLE_GEMFILE'] = gemfile + require 'bundler' + Bundler.setup +end + +$:.unshift File.expand_path('../../../../lib', __FILE__)
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/routes.rb b/railties/lib/rails/generators/rails/plugin_new/templates/rails/routes.rb new file mode 100644 index 0000000000..730ee31c3d --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/rails/routes.rb @@ -0,0 +1,4 @@ +Rails.application.routes.draw do + + mount <%= camelized %>::Engine => "/<%= name %>" +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/script/rails.tt b/railties/lib/rails/generators/rails/plugin_new/templates/script/rails.tt new file mode 100644 index 0000000000..ebd5a77dd5 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/script/rails.tt @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +ENGINE_PATH = File.expand_path('../..', __FILE__) +load File.expand_path('../../<%= dummy_path %>/script/rails', __FILE__) diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/test/%name%_test.rb b/railties/lib/rails/generators/rails/plugin_new/templates/test/%name%_test.rb new file mode 100644 index 0000000000..0a8bbd4aaf --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/test/%name%_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class <%= camelized %>Test < ActiveSupport::TestCase + test "truth" do + assert_kind_of Module, <%= camelized %> + end +end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb new file mode 100644 index 0000000000..d06fe7cbd0 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb @@ -0,0 +1,11 @@ +require 'test_helper' + +class NavigationTest < ActionDispatch::IntegrationTest + fixtures :all + + # Replace this with your real tests. + test "the truth" do + assert true + end +end + diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb new file mode 100644 index 0000000000..7b61047e77 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb @@ -0,0 +1,15 @@ +# Configure Rails Envinronment +ENV["RAILS_ENV"] = "test" + +require File.expand_path("../dummy/config/environment.rb", __FILE__) +require "rails/test_help" + +Rails.backtrace_cleaner.remove_silencers! + +<% if full? && !options[:skip_active_record] -%> +# Run any available migration from application +ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__) +<% end -%> + +# Load support files +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } diff --git a/railties/lib/rails/generators/rails/resource/resource_generator.rb b/railties/lib/rails/generators/rails/resource/resource_generator.rb index 8a943013d3..c7345f3cfb 100644 --- a/railties/lib/rails/generators/rails/resource/resource_generator.rb +++ b/railties/lib/rails/generators/rails/resource/resource_generator.rb @@ -16,9 +16,9 @@ module Rails def add_resource_route return if options[:actions].present? - route_config = class_path.collect{|namespace| "namespace :#{namespace} do " }.join(" ") + route_config = regular_class_path.collect{|namespace| "namespace :#{namespace} do " }.join(" ") route_config << "resources :#{file_name.pluralize}" - route_config << " end" * class_path.size + route_config << " end" * regular_class_path.size route route_config end 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 b21340f755..b5317a055b 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -1,3 +1,4 @@ +<% module_namespacing do -%> class <%= controller_class_name %>Controller < ApplicationController # GET <%= route_url %> # GET <%= route_url %>.xml @@ -81,3 +82,4 @@ class <%= controller_class_name %>Controller < ApplicationController end end end +<% end -%> diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb index 829f4b200a..d6ccfc496a 100644 --- a/railties/lib/rails/generators/resource_helpers.rb +++ b/railties/lib/rails/generators/resource_helpers.rb @@ -17,7 +17,7 @@ module Rails def initialize(*args) #:nodoc: super - if name == name.pluralize && !options[:force_plural] + if name == name.pluralize && name.singularize != name.pluralize && !options[:force_plural] unless ResourceHelpers.skip_warn say "Plural version of the model detected, using singularized version. Override with --force-plural." ResourceHelpers.skip_warn = true @@ -34,7 +34,7 @@ module Rails attr_reader :controller_name def controller_class_path - @class_path + class_path end def controller_file_name @@ -46,7 +46,7 @@ module Rails end def controller_class_name - @controller_class_name ||= (controller_class_path + [controller_file_name]).map!{ |m| m.camelize }.join('::') + (controller_class_path + [controller_file_name]).map!{ |m| m.camelize }.join('::') end def controller_i18n_scope 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 f23e495450..964d59d84c 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 @@ -1,5 +1,6 @@ require 'test_helper' +<% module_namespacing do -%> class <%= controller_class_name %>ControllerTest < ActionController::TestCase setup do @<%= singular_table_name %> = <%= table_name %>(:one) @@ -47,3 +48,4 @@ class <%= controller_class_name %>ControllerTest < ActionController::TestCase assert_redirected_to <%= index_helper %>_path end end +<% end -%> diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index 6cbd1f21c0..d05e031f56 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -1,5 +1,4 @@ require "cgi" -require "active_support/core_ext/cgi" module Rails module Info diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index c76bc83377..030a838dc1 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -97,7 +97,7 @@ module Rails # If your railtie has rake tasks, you can tell Rails to load them through the method # rake tasks: # - # class MyRailtie < Railtie + # class MyRailtie < Rails::Railtie # rake_tasks do # load "path/to/my_railtie.tasks" # end @@ -107,7 +107,7 @@ module Rails # your generators at a different location, you can specify in your Railtie a block which # will load them during normal generators lookup: # - # class MyRailtie < Railtie + # class MyRailtie < Rails::Railtie # generators do # require "path/to/my_railtie_generator" # end diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 38f2f651f4..f81002328f 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -39,14 +39,3 @@ class ActionDispatch::IntegrationTest @routes = Rails.application.routes end end - -begin - require_library_or_gem 'ruby-debug' - Debugger.start - if Debugger.respond_to?(:settings) - Debugger.settings[:autoeval] = true - Debugger.settings[:autolist] = 1 - end -rescue LoadError - # ruby-debug wasn't available so neither can the debugging be -end diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb index 0213d46254..b076881c21 100644 --- a/railties/lib/rails/version.rb +++ b/railties/lib/rails/version.rb @@ -3,8 +3,8 @@ module Rails MAJOR = 3 MINOR = 1 TINY = 0 - BUILD = "beta" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index d26c1bcdbc..c3793d3ac3 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.has_rdoc = false s.add_dependency('rake', '>= 0.8.7') - s.add_dependency('thor', '~> 0.14.3') + s.add_dependency('thor', '~> 0.14.4') s.add_dependency('activesupport', version) s.add_dependency('actionpack', version) end diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb index 551e966c85..8b840fffd0 100644 --- a/railties/test/application/generators_test.rb +++ b/railties/test/application/generators_test.rb @@ -25,6 +25,11 @@ module ApplicationTests yield app_const.config end + test "allow running plugin new generator inside Rails app directory" do + FileUtils.cd(rails_root){ `ruby script/rails plugin new vendor/plugins/bukkits` } + assert File.exist?(File.join(rails_root, "vendor/plugins/bukkits/test/dummy/config/application.rb")) + end + test "generators default values" do with_bare_config do |c| assert_equal(true, c.generators.colorize_logging) diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 6970ea7b7a..475091f789 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -65,6 +65,66 @@ module ApplicationTests assert_equal ["notify"], Foo.action_methods end + test "allows to not load all helpers for controllers" do + add_to_config "config.action_controller.include_all_helpers = false" + + app_file "app/controllers/application_controller.rb", <<-RUBY + class ApplicationController < ActionController::Base + end + RUBY + + app_file "app/controllers/foo_controller.rb", <<-RUBY + class FooController < ApplicationController + def included_helpers + render :inline => "<%= from_app_helper -%> <%= from_foo_helper %>" + end + + def not_included_helper + render :inline => "<%= respond_to?(:from_bar_helper) -%>" + end + end + RUBY + + app_file "app/helpers/application_helper.rb", <<-RUBY + module ApplicationHelper + def from_app_helper + "from_app_helper" + end + end + RUBY + + app_file "app/helpers/foo_helper.rb", <<-RUBY + module FooHelper + def from_foo_helper + "from_foo_helper" + end + end + RUBY + + app_file "app/helpers/bar_helper.rb", <<-RUBY + module BarHelper + def from_bar_helper + "from_bar_helper" + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + AppTemplate::Application.routes.draw do + match "/:controller(/:action)" + end + RUBY + + require 'rack/test' + extend Rack::Test::Methods + + get "/foo/included_helpers" + assert_equal "from_app_helper from_foo_helper", last_response.body + + get "/foo/not_included_helper" + assert_equal "false", last_response.body + end + # AD test "action_dispatch extensions are applied to ActionDispatch" do add_to_config "config.action_dispatch.tld_length = 2" diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 8e527236ea..23cd2378c7 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -34,9 +34,34 @@ module ApplicationTests assert_match "SuperMiddleware", Dir.chdir(app_path){ `rake middleware` } end + def test_initializers_are_executed_in_rake_tasks + add_to_config <<-RUBY + initializer "do_something" do + puts "Doing something..." + end + + rake_tasks do + task :do_nothing => :environment do + end + end + RUBY + + output = Dir.chdir(app_path){ `rake do_nothing` } + assert_match "Doing something...", output + end + def test_code_statistics_sanity assert_match "Code LOC: 5 Test LOC: 0 Code to Test Ratio: 1:0.0", Dir.chdir(app_path){ `rake stats` } end + + def test_rake_routes_output_strips_anchors_from_http_verbs + app_file "config/routes.rb", <<-RUBY + AppTemplate::Application.routes.draw do + get '/cart', :to => 'cart#show' + end + RUBY + assert_match 'cart GET /cart(.:format)', Dir.chdir(app_path){ `rake routes` } + end end end diff --git a/railties/test/fixtures/lib/empty_builder.rb b/railties/test/fixtures/lib/app_builders/empty_builder.rb index babd9c2461..babd9c2461 100644 --- a/railties/test/fixtures/lib/empty_builder.rb +++ b/railties/test/fixtures/lib/app_builders/empty_builder.rb diff --git a/railties/test/fixtures/lib/app_builders/simple_builder.rb b/railties/test/fixtures/lib/app_builders/simple_builder.rb new file mode 100644 index 0000000000..993d3a2aa2 --- /dev/null +++ b/railties/test/fixtures/lib/app_builders/simple_builder.rb @@ -0,0 +1,7 @@ +class AppBuilder + def gitignore + create_file ".gitignore", <<-R.strip +foobar + R + end +end diff --git a/railties/test/fixtures/lib/app_builders/tweak_builder.rb b/railties/test/fixtures/lib/app_builders/tweak_builder.rb new file mode 100644 index 0000000000..cb50be01cb --- /dev/null +++ b/railties/test/fixtures/lib/app_builders/tweak_builder.rb @@ -0,0 +1,7 @@ +class AppBuilder < Rails::AppBuilder + def gitignore + create_file ".gitignore", <<-R.strip +foobar + R + end +end diff --git a/railties/test/fixtures/lib/create_test_dummy_template.rb b/railties/test/fixtures/lib/create_test_dummy_template.rb new file mode 100644 index 0000000000..e4378bbd1a --- /dev/null +++ b/railties/test/fixtures/lib/create_test_dummy_template.rb @@ -0,0 +1 @@ +create_dummy_app("spec/dummy") diff --git a/railties/test/fixtures/lib/plugin_builders/empty_builder.rb b/railties/test/fixtures/lib/plugin_builders/empty_builder.rb new file mode 100644 index 0000000000..5c5607621c --- /dev/null +++ b/railties/test/fixtures/lib/plugin_builders/empty_builder.rb @@ -0,0 +1,2 @@ +class PluginBuilder +end diff --git a/railties/test/fixtures/lib/plugin_builders/simple_builder.rb b/railties/test/fixtures/lib/plugin_builders/simple_builder.rb new file mode 100644 index 0000000000..08f6c5535d --- /dev/null +++ b/railties/test/fixtures/lib/plugin_builders/simple_builder.rb @@ -0,0 +1,7 @@ +class PluginBuilder + def gitignore + create_file ".gitignore", <<-R.strip +foobar + R + end +end diff --git a/railties/test/fixtures/lib/plugin_builders/spec_builder.rb b/railties/test/fixtures/lib/plugin_builders/spec_builder.rb new file mode 100644 index 0000000000..aa18c7ddaa --- /dev/null +++ b/railties/test/fixtures/lib/plugin_builders/spec_builder.rb @@ -0,0 +1,19 @@ +class PluginBuilder < Rails::PluginBuilder + def test + create_file "spec/spec_helper.rb" + append_file "Rakefile", <<-EOF +# spec tasks in rakefile + +task :default => :spec + EOF + end + + def generate_test_dummy + dummy_path("spec/dummy") + super + end + + def skip_test_unit? + true + end +end diff --git a/railties/test/fixtures/lib/plugin_builders/tweak_builder.rb b/railties/test/fixtures/lib/plugin_builders/tweak_builder.rb new file mode 100644 index 0000000000..1e801409a4 --- /dev/null +++ b/railties/test/fixtures/lib/plugin_builders/tweak_builder.rb @@ -0,0 +1,7 @@ +class PluginBuilder < Rails::PluginBuilder + def gitignore + create_file ".gitignore", <<-R.strip +foobar + R + end +end diff --git a/railties/test/fixtures/lib/simple_builder.rb b/railties/test/fixtures/lib/simple_builder.rb deleted file mode 100644 index 47dcdc0d96..0000000000 --- a/railties/test/fixtures/lib/simple_builder.rb +++ /dev/null @@ -1,7 +0,0 @@ -class AppBuilder - def configru - create_file "config.ru", <<-R.strip -run proc { |env| [200, { "Content-Type" => "text/html" }, ["Hello World"]] } - R - end -end
\ No newline at end of file diff --git a/railties/test/fixtures/lib/tweak_builder.rb b/railties/test/fixtures/lib/tweak_builder.rb deleted file mode 100644 index eed20ecc9b..0000000000 --- a/railties/test/fixtures/lib/tweak_builder.rb +++ /dev/null @@ -1,7 +0,0 @@ -class AppBuilder < Rails::AppBuilder - def configru - create_file "config.ru", <<-R.strip -run proc { |env| [200, { "Content-Type" => "text/html" }, ["Hello World"]] } - R - end -end
\ No newline at end of file diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index c7339cb8d2..7faa674a81 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' +require 'generators/shared_generator_tests.rb' DEFAULT_APP_FILES = %w( .gitignore @@ -40,36 +41,10 @@ DEFAULT_APP_FILES = %w( class AppGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments [destination_root] + include SharedGeneratorTests - def setup - Rails.application = TestApp::Application - super - Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) - @bundle_command = File.basename(Thor::Util.ruby_command).sub(/ruby/, 'bundle') - - Kernel::silence_warnings do - Thor::Base.shell.send(:attr_accessor, :always_force) - @shell = Thor::Base.shell.new - @shell.send(:always_force=, true) - end - end - - def teardown - super - Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) - Rails.application = TestApp::Application.instance - end - - def test_application_skeleton_is_created - run_generator - - DEFAULT_APP_FILES.each{ |path| assert_file path } - end - - def test_application_generate_pretend - run_generator ["testapp", "--pretend"] - - DEFAULT_APP_FILES.each{ |path| assert_no_file path } + def default_files + ::DEFAULT_APP_FILES end def test_application_controller_and_layout_files @@ -78,36 +53,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_file "public/stylesheets/application.css" end - def test_options_before_application_name_raises_an_error - content = capture(:stderr){ run_generator(["--skip-active-record", destination_root]) } - assert_equal "Options should be given after the application name. For details run: rails --help\n", content - end - - def test_name_collision_raises_an_error - reserved_words = %w[application destroy plugin runner test] - reserved_words.each do |reserved| - content = capture(:stderr){ run_generator [File.join(destination_root, reserved)] } - assert_equal "Invalid application name #{reserved}. Please give a name which does not match one of the reserved rails words.\n", content - end - end - - def test_invalid_database_option_raises_an_error - content = capture(:stderr){ run_generator([destination_root, "-d", "unknown"]) } - assert_match /Invalid value for \-\-database option/, content - end - def test_invalid_application_name_raises_an_error content = capture(:stderr){ run_generator [File.join(destination_root, "43-things")] } assert_equal "Invalid application name 43-things. Please give a name which does not start with numbers.\n", content end - def test_application_name_raises_an_error_if_name_already_used_constant - %w{ String Hash Class Module Set Symbol }.each do |ruby_class| - content = capture(:stderr){ run_generator [File.join(destination_root, ruby_class)] } - assert_equal "Invalid application name #{ruby_class}, constant #{ruby_class} is already in use. Please choose another application name.\n", content - end - end - def test_invalid_application_name_is_fixed run_generator [File.join(destination_root, "things-43")] assert_file "things-43/config/environment.rb", /Things43::Application\.initialize!/ @@ -133,11 +83,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "myapp_moved/config/environment.rb", /Myapp::Application\.initialize!/ assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/ end - + def test_rails_update_generates_correct_session_key app_root = File.join(destination_root, 'myapp') run_generator [app_root] - + Rails.application.config.root = app_root Rails.application.class.stubs(:name).returns("Myapp") Rails.application.stubs(:is_a?).returns(Rails::Application) @@ -187,12 +137,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "public/javascripts/rails.js" assert_file "test" end - - def test_test_unit_is_skipped_if_required - run_generator [destination_root, "--skip-test-unit"] - assert_no_file "test" - end - + def test_javascript_is_skipped_if_required run_generator [destination_root, "--skip-javascript"] assert_file "config/application.rb", /^\s+config\.action_view\.javascript_expansions\[:defaults\]\s+=\s+%w\(\)/ @@ -200,7 +145,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_file "public/javascripts/prototype.js" assert_no_file "public/javascripts/rails.js" end - + def test_config_prototype_javascript_library run_generator [destination_root, "-j", "prototype"] assert_file "config/application.rb", /#\s+config\.action_view\.javascript_expansions\[:defaults\]\s+=\s+%w\(jquery rails\)/ @@ -208,7 +153,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "public/javascripts/prototype.js" assert_file "public/javascripts/rails.js", /prototype/ end - + def test_config_jquery_javascript_library run_generator [destination_root, "-j", "jquery"] assert_file "config/application.rb", /^\s+config\.action_view\.javascript_expansions\[:defaults\]\s+=\s+%w\(jquery rails\)/ @@ -217,36 +162,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "public/javascripts/rails.js", /jQuery/ end - def test_shebang_is_added_to_rails_file - run_generator [destination_root, "--ruby", "foo/bar/baz"] - assert_file "script/rails", /#!foo\/bar\/baz/ - end - - def test_shebang_when_is_the_same_as_default_use_env - run_generator [destination_root, "--ruby", Thor::Util.ruby_command] - assert_file "script/rails", /#!\/usr\/bin\/env/ - end - def test_template_from_dir_pwd FileUtils.cd(Rails.root) assert_match /It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"]) end - def test_template_raises_an_error_with_invalid_path - content = capture(:stderr){ run_generator([destination_root, "-m", "non/existant/path"]) } - assert_match /The template \[.*\] could not be loaded/, content - assert_match /non\/existant\/path/, content - end - - def test_template_is_executed_when_supplied - path = "http://gist.github.com/103208.txt" - template = %{ say "It works!" } - template.instance_eval "def read; self; end" # Make the string respond to read - - generator([destination_root], :template => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - assert_match /It works!/, silence(:stdout){ generator.invoke_all } - end - def test_usage_read_from_file File.expects(:read).returns("USAGE FROM FILE") assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc @@ -266,17 +186,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file 'lib/test_file.rb', 'heres test data' end - def test_dev_option - generator([destination_root], :dev => true).expects(:run).with("#{@bundle_command} install") - silence(:stdout){ generator.invoke_all } - rails_path = File.expand_path('../../..', Rails.root) - assert_file 'Gemfile', /^gem\s+["']rails["'],\s+:path\s+=>\s+["']#{Regexp.escape(rails_path)}["']$/ - end - - def test_edge_option - generator([destination_root], :edge => true).expects(:run).with("#{@bundle_command} install") - silence(:stdout){ generator.invoke_all } - assert_file 'Gemfile', /^gem\s+["']rails["'],\s+:git\s+=>\s+["']#{Regexp.escape("git://github.com/rails/rails.git")}["']$/ + def test_test_unit_is_removed_from_frameworks_if_skip_test_unit_is_given + run_generator [destination_root, "--skip-test-unit"] + assert_file "config/application.rb" do |file| + assert_match /config.generators.test_framework = false/, file + end end protected @@ -292,62 +206,21 @@ class CustomAppGeneratorTest < Rails::Generators::TestCase tests Rails::Generators::AppGenerator arguments [destination_root] + include SharedCustomGeneratorTests - def setup - Rails.application = TestApp::Application - super - Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) - @bundle_command = File.basename(Thor::Util.ruby_command).sub(/ruby/, 'bundle') - end - - def teardown - super - Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) - Object.class_eval { remove_const :AppBuilder if const_defined?(:AppBuilder) } - Rails.application = TestApp::Application.instance - end - - def test_builder_option_with_empty_app_builder - FileUtils.cd(Rails.root) - run_generator([destination_root, "-b", "#{Rails.root}/lib/empty_builder.rb"]) - DEFAULT_APP_FILES.each{ |path| assert_no_file path } - end - - def test_builder_option_with_simple_app_builder - FileUtils.cd(Rails.root) - run_generator([destination_root, "-b", "#{Rails.root}/lib/simple_builder.rb"]) - (DEFAULT_APP_FILES - ['config.ru']).each{ |path| assert_no_file path } - assert_file "config.ru", %[run proc { |env| [200, { "Content-Type" => "text/html" }, ["Hello World"]] }] - end - - def test_builder_option_with_relative_path - here = File.expand_path(File.dirname(__FILE__)) - FileUtils.cd(here) - run_generator([destination_root, "-b", "../fixtures/lib/simple_builder.rb"]) - (DEFAULT_APP_FILES - ['config.ru']).each{ |path| assert_no_file path } - assert_file "config.ru", %[run proc { |env| [200, { "Content-Type" => "text/html" }, ["Hello World"]] }] +protected + def default_files + ::DEFAULT_APP_FILES end - def test_builder_option_with_tweak_app_builder - FileUtils.cd(Rails.root) - run_generator([destination_root, "-b", "#{Rails.root}/lib/tweak_builder.rb"]) - DEFAULT_APP_FILES.each{ |path| assert_file path } - assert_file "config.ru", %[run proc { |env| [200, { "Content-Type" => "text/html" }, ["Hello World"]] }] + def builders_dir + "app_builders" end - def test_builder_option_with_http - path = "http://gist.github.com/103208.txt" - template = "class AppBuilder; end" - template.instance_eval "def read; self; end" # Make the string respond to read - - generator([destination_root], :builder => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - capture(:stdout) { generator.invoke_all } - - DEFAULT_APP_FILES.each{ |path| assert_no_file path } + def builder_class + :AppBuilder end -protected - def action(*args, &block) silence(:stdout){ generator.send(*args, &block) } end diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index f9d1e42d24..288ec30460 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -34,12 +34,12 @@ class MigrationGeneratorTest < Rails::Generators::TestCase run_generator [migration, "title:string", "body:text"] assert_migration "db/migrate/#{migration}.rb" do |content| - assert_class_method :up, content do |up| + assert_method :up, content do |up| assert_match /add_column :posts, :title, :string/, up assert_match /add_column :posts, :body, :text/, up end - assert_class_method :down, content do |down| + assert_method :down, content do |down| assert_match /remove_column :posts, :title/, down assert_match /remove_column :posts, :body/, down end @@ -51,12 +51,12 @@ class MigrationGeneratorTest < Rails::Generators::TestCase run_generator [migration, "title:string", "body:text"] assert_migration "db/migrate/#{migration}.rb" do |content| - assert_class_method :up, content do |up| + assert_method :up, content do |up| assert_match /remove_column :posts, :title/, up assert_match /remove_column :posts, :body/, up end - assert_class_method :down, content do |down| + assert_method :down, content do |down| assert_match /add_column :posts, :title, :string/, down assert_match /add_column :posts, :body, :text/, down end @@ -68,11 +68,11 @@ class MigrationGeneratorTest < Rails::Generators::TestCase run_generator [migration, "title:string", "content:text"] assert_migration "db/migrate/#{migration}.rb" do |content| - assert_class_method :up, content do |up| + assert_method :up, content do |up| assert_match /^\s*$/, up end - assert_class_method :down, content do |down| + assert_method :down, content do |down| assert_match /^\s*$/, down end end diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 52e08cf2dd..8a0f560bc8 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -99,13 +99,13 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator ["product", "name:string", "supplier_id:integer"] assert_migration "db/migrate/create_products.rb" do |m| - assert_class_method :up, m do |up| + assert_method :up, m do |up| assert_match /create_table :products/, up assert_match /t\.string :name/, up assert_match /t\.integer :supplier_id/, up end - assert_class_method :down, m do |down| + assert_method :down, m do |down| assert_match /drop_table :products/, down end end @@ -141,7 +141,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator ["account", "--no-timestamps"] assert_migration "db/migrate/create_accounts.rb" do |m| - assert_class_method :up, m do |up| + assert_method :up, m do |up| assert_no_match /t.timestamps/, up end end diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index d1190fd17d..d61a02d32f 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -3,6 +3,7 @@ require 'rails/generators/rails/controller/controller_generator' require 'rails/generators/rails/model/model_generator' require 'rails/generators/rails/observer/observer_generator' require 'rails/generators/mailer/mailer_generator' +require 'rails/generators/rails/scaffold/scaffold_generator' class NamespacedGeneratorTestCase < Rails::Generators::TestCase def setup @@ -202,3 +203,155 @@ class NamespacedMailerGeneratorTest < NamespacedGeneratorTestCase assert_file "app/views/test_app/notifier" end end + +class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase + include GeneratorsTestHelper + arguments %w(product_line title:string price:integer) + tests Rails::Generators::ScaffoldGenerator + + setup :copy_routes + + def test_scaffold_on_invoke + run_generator + + # Model + assert_file "app/models/test_app/product_line.rb", /module TestApp\n class ProductLine < ActiveRecord::Base/ + assert_file "test/unit/test_app/product_line_test.rb", /module TestApp\n class ProductLineTest < ActiveSupport::TestCase/ + assert_file "test/fixtures/test_app/product_lines.yml" + assert_migration "db/migrate/create_test_app_product_lines.rb" + + # Route + assert_file "config/routes.rb" do |route| + assert_match(/resources :product_lines$/, route) + end + + # Controller + assert_file "app/controllers/test_app/product_lines_controller.rb" do |content| + assert_match(/module TestApp\n class ProductLinesController < ApplicationController/, content) + end + + assert_file "test/functional/test_app/product_lines_controller_test.rb", + /module TestApp\n class ProductLinesControllerTest < ActionController::TestCase/ + + # Views + %w( + index + edit + new + show + _form + ).each { |view| assert_file "app/views/test_app/product_lines/#{view}.html.erb" } + assert_no_file "app/views/layouts/test_app/product_lines.html.erb" + + # Helpers + assert_file "app/helpers/test_app/product_lines_helper.rb" + assert_file "test/unit/helpers/test_app/product_lines_helper_test.rb" + + # Stylesheets + assert_file "public/stylesheets/scaffold.css" + end + + def test_scaffold_on_revoke + run_generator + run_generator ["product_line"], :behavior => :revoke + + # Model + assert_no_file "app/models/test_app/product_line.rb" + assert_no_file "test/unit/test_app/product_line_test.rb" + assert_no_file "test/fixtures/test_app/product_lines.yml" + assert_no_migration "db/migrate/create_test_app_product_lines.rb" + + # Route + assert_file "config/routes.rb" do |route| + assert_no_match(/resources :product_lines$/, route) + end + + # Controller + assert_no_file "app/controllers/test_app/product_lines_controller.rb" + assert_no_file "test/functional/test_app/product_lines_controller_test.rb" + + # Views + assert_no_file "app/views/test_app/product_lines" + assert_no_file "app/views/test_app/layouts/product_lines.html.erb" + + # Helpers + assert_no_file "app/helpers/test_app/product_lines_helper.rb" + assert_no_file "test/unit/helpers/test_app/product_lines_helper_test.rb" + + # 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/test_app/admin.rb", /module TestApp\n module Admin/ + assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ActiveRecord::Base/ + assert_file "test/unit/test_app/admin/role_test.rb", /module TestApp\n class Admin::RoleTest < ActiveSupport::TestCase/ + assert_file "test/fixtures/test_app/admin/roles.yml" + assert_migration "db/migrate/create_test_app_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/test_app/admin/roles_controller.rb" do |content| + assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content) + end + + assert_file "test/functional/test_app/admin/roles_controller_test.rb", + /module TestApp\n class Admin::RolesControllerTest < ActionController::TestCase/ + + # Views + %w( + index + edit + new + show + _form + ).each { |view| assert_file "app/views/test_app/admin/roles/#{view}.html.erb" } + assert_no_file "app/views/layouts/admin/roles.html.erb" + + # Helpers + assert_file "app/helpers/test_app/admin/roles_helper.rb" + assert_file "test/unit/helpers/test_app/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/test_app/admin.rb" # ( should not be remove ) + assert_no_file "app/models/test_app/admin/role.rb" + assert_no_file "test/unit/test_app/admin/role_test.rb" + assert_no_file "test/fixtures/test_app/admin/roles.yml" + assert_no_migration "db/migrate/create_test_app_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/test_app/admin/roles_controller.rb" + assert_no_file "test/functional/test_app/admin/roles_controller_test.rb" + + # Views + assert_no_file "app/views/test_app/admin/roles" + assert_no_file "app/views/layouts/test_app/admin/roles.html.erb" + + # Helpers + assert_no_file "app/helpers/test_app/admin/roles_helper.rb" + assert_no_file "test/unit/helpers/test_app/admin/roles_helper_test.rb" + + # Stylesheets (should not be removed) + assert_file "public/stylesheets/scaffold.css" + end +end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index c1f646f7c1..e6686a6af4 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -6,7 +6,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase arguments %w(plugin_fu) def test_plugin_skeleton_is_created - run_generator + silence(:stderr) { run_generator } year = Date.today.year %w( @@ -36,30 +36,36 @@ class PluginGeneratorTest < Rails::Generators::TestCase end def test_invokes_default_test_framework - run_generator + silence(:stderr) { run_generator } assert_file "vendor/plugins/plugin_fu/test/plugin_fu_test.rb", /class PluginFuTest < ActiveSupport::TestCase/ assert_file "vendor/plugins/plugin_fu/test/test_helper.rb" end def test_logs_if_the_test_framework_cannot_be_found - content = run_generator ["plugin_fu", "--test-framework=rspec"] + content = nil + silence(:stderr) { content = run_generator ["plugin_fu", "--test-framework=rspec"] } assert_match /rspec \[not found\]/, content end def test_creates_tasks_if_required - run_generator ["plugin_fu", "--tasks"] + silence(:stderr) { run_generator ["plugin_fu", "--tasks"] } assert_file "vendor/plugins/plugin_fu/lib/tasks/plugin_fu_tasks.rake" end def test_creates_generator_if_required - run_generator ["plugin_fu", "--generator"] + silence(:stderr) { run_generator ["plugin_fu", "--generator"] } assert_file "vendor/plugins/plugin_fu/lib/generators/templates" assert_file "vendor/plugins/plugin_fu/lib/generators/plugin_fu_generator.rb", /class PluginFuGenerator < Rails::Generators::NamedBase/ end def test_plugin_generator_on_revoke - run_generator + silence(:stderr) { run_generator } run_generator ["plugin_fu"], :behavior => :revoke end + + def test_deprecation + output = capture(:stderr) { run_generator } + assert_match /Plugin generator is deprecated, please use 'rails plugin new' command to generate plugin structure./, output + end end diff --git a/railties/test/generators/plugin_new_generator_test.rb b/railties/test/generators/plugin_new_generator_test.rb new file mode 100644 index 0000000000..2105585272 --- /dev/null +++ b/railties/test/generators/plugin_new_generator_test.rb @@ -0,0 +1,196 @@ +require 'abstract_unit' +require 'generators/generators_test_helper' +require 'rails/generators/rails/plugin_new/plugin_new_generator' +require 'generators/shared_generator_tests.rb' + +DEFAULT_PLUGIN_FILES = %w( + .gitignore + Gemfile + Rakefile + bukkits.gemspec + MIT-LICENSE + lib + lib/bukkits.rb + lib/tasks/bukkits_tasks.rake + script/rails + test/bukkits_test.rb + test/test_helper.rb + test/dummy +) + +class PluginNewGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + destination File.join(Rails.root, "tmp/bukkits") + arguments [destination_root] + include SharedGeneratorTests + + def default_files + ::DEFAULT_PLUGIN_FILES + end + + def test_invalid_plugin_name_raises_an_error + content = capture(:stderr){ run_generator [File.join(destination_root, "43-things")] } + assert_equal "Invalid plugin name 43-things. Please give a name which does not start with numbers.\n", content + end + + def test_invalid_plugin_name_is_fixed + run_generator [File.join(destination_root, "things-43")] + assert_file "things-43/lib/things-43.rb", /module Things43/ + end + + def test_generating_test_files + run_generator + assert_file "test/test_helper.rb" + assert_file "test/bukkits_test.rb", /assert_kind_of Module, Bukkits/ + end + + def test_generating_test_files_in_full_mode + run_generator [destination_root, "--full"] + assert_directory "test/integration/" + + assert_file "test/integration/navigation_test.rb", /ActionDispatch::IntegrationTest/ + end + + def test_ensure_that_plugin_options_are_not_passed_to_app_generator + FileUtils.cd(Rails.root) + assert_no_match /It works from file!.*It works_from_file/, run_generator([destination_root, "-m", "lib/template.rb"]) + end + + def test_ensure_that_test_dummy_can_be_generated_from_a_template + FileUtils.cd(Rails.root) + run_generator([destination_root, "-m", "lib/create_test_dummy_template.rb", "--skip-test-unit"]) + assert_file "spec/dummy" + assert_no_file "test" + end + + def test_database_entry_is_assed_by_default_in_full_mode + run_generator([destination_root, "--full"]) + assert_file "test/dummy/config/database.yml", /sqlite/ + assert_file "Gemfile", /^gem\s+["']sqlite3-ruby["'],\s+:require\s+=>\s+["']sqlite3["']$/ + end + + def test_config_another_database + run_generator([destination_root, "-d", "mysql", "--full"]) + assert_file "test/dummy/config/database.yml", /mysql/ + assert_file "Gemfile", /^gem\s+["']mysql2["']$/ + end + + def test_active_record_is_removed_from_frameworks_if_skip_active_record_is_given + run_generator [destination_root, "--skip-active-record"] + assert_file "test/dummy/config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ + end + + def test_ensure_that_skip_active_record_option_is_passed_to_app_generator + run_generator [destination_root, "--skip_active_record"] + assert_no_file "test/dummy/config/database.yml" + assert_no_match /ActiveRecord/, File.read(File.join(destination_root, "test/test_helper.rb")) + end + + def test_ensure_that_database_option_is_passed_to_app_generator + run_generator [destination_root, "--database", "postgresql"] + assert_file "test/dummy/config/database.yml", /postgres/ + end + + def test_ensure_that_javascript_option_is_passed_to_app_generator + run_generator [destination_root, "--javascript", "jquery"] + assert_file "test/dummy/public/javascripts/jquery.js" + end + + def test_ensure_that_skip_javascript_option_is_passed_to_app_generator + run_generator [destination_root, "--skip_javascript"] + assert_no_file "test/dummy/public/javascripts/prototype.js" + end + + def test_template_from_dir_pwd + FileUtils.cd(Rails.root) + assert_match /It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"]) + end + + def test_ensure_that_tests_works + run_generator + FileUtils.cd destination_root + `bundle install` + assert_match /1 tests, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test` + end + + def test_ensure_that_tests_works_in_full_mode + run_generator [destination_root, "--full"] + FileUtils.cd destination_root + `bundle install` + assert_match /2 tests, 2 assertions, 0 failures, 0 errors/, `bundle exec rake test` + end + + def test_creating_engine_in_full_mode + run_generator [destination_root, "--full"] + assert_file "lib/bukkits/engine.rb", /module Bukkits\n class Engine < Rails::Engine\n end\nend/ + assert_file "lib/bukkits.rb", /require "bukkits\/engine"/ + end + + def test_being_quiet_while_creating_dummy_application + assert_no_match /create\s+config\/application.rb/, run_generator + end + + def test_create_mountable_application_with_mountable_option + run_generator [destination_root, "--mountable"] + assert_file "config/routes.rb", /Bukkits::Engine.routes.draw do/ + assert_file "lib/bukkits/engine.rb", /isolate_namespace Bukkits/ + assert_file "test/dummy/config/routes.rb", /mount Bukkits::Engine => "\/bukkits"/ + assert_file "app/controllers/bukkits/application_controller.rb", /module Bukkits\n class ApplicationController < ActionController::Base/ + assert_file "app/helpers/bukkits/application_helper.rb", /module Bukkits\n module ApplicationHelper/ + end + + def test_passing_dummy_path_as_a_parameter + run_generator [destination_root, "--dummy_path", "spec/dummy"] + assert_file "spec/dummy" + assert_file "spec/dummy/config/application.rb" + assert_no_file "test/dummy" + end + + def test_skipping_gemspec + run_generator [destination_root, "--skip-gemspec"] + assert_no_file "bukkits.gemspec" + end + +protected + + def action(*args, &block) + silence(:stdout){ generator.send(*args, &block) } + end + +end + +class CustomPluginGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + tests Rails::Generators::PluginNewGenerator + + destination File.join(Rails.root, "tmp/bukkits") + arguments [destination_root] + include SharedCustomGeneratorTests + + def test_overriding_test_framework + FileUtils.cd(destination_root) + run_generator([destination_root, "-b", "#{Rails.root}/lib/plugin_builders/spec_builder.rb"]) + assert_file 'spec/spec_helper.rb' + assert_file 'spec/dummy' + assert_file 'Rakefile', /task :default => :spec/ + assert_file 'Rakefile', /# spec tasks in rakefile/ + assert_file 'script/rails', %r{spec/dummy} + end + +protected + def default_files + ::DEFAULT_PLUGIN_FILES + end + + def builder_class + :PluginBuilder + end + + def builders_dir + "plugin_builders" + end + + def action(*args, &block) + silence(:stdout){ generator.send(*args, &block) } + end +end diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb index 55d5bd6f83..71b3619351 100644 --- a/railties/test/generators/resource_generator_test.rb +++ b/railties/test/generators/resource_generator_test.rb @@ -73,6 +73,11 @@ class ResourceGeneratorTest < Rails::Generators::TestCase assert_no_match /Plural version of the model detected/, content end + def test_mass_nouns_do_not_throw_warnings + content = run_generator ["sheep".freeze] + assert_no_match /Plural version of the model detected/, content + end + def test_route_is_removed_on_revoke run_generator run_generator ["account"], :behavior => :revoke diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb new file mode 100644 index 0000000000..d117656fbd --- /dev/null +++ b/railties/test/generators/shared_generator_tests.rb @@ -0,0 +1,187 @@ +module SharedGeneratorTests + def setup + Rails.application = TestApp::Application + super + Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) + @bundle_command = File.basename(Thor::Util.ruby_command).sub(/ruby/, 'bundle') + + Kernel::silence_warnings do + Thor::Base.shell.send(:attr_accessor, :always_force) + @shell = Thor::Base.shell.new + @shell.send(:always_force=, true) + end + end + + def teardown + super + Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) + Rails.application = TestApp::Application.instance + end + + def test_skeleton_is_created + run_generator + + default_files.each{ |path| assert_file path } + end + + def test_plugin_new_generate_pretend + run_generator ["testapp", "--pretend"] + + default_files.each{ |path| assert_no_file path } + end + + def test_invalid_database_option_raises_an_error + content = capture(:stderr){ run_generator([destination_root, "-d", "unknown"]) } + assert_match /Invalid value for \-\-database option/, content + end + + def test_test_unit_is_skipped_if_required + run_generator [destination_root, "--skip-test-unit"] + assert_no_file "test" + end + + def test_options_before_application_name_raises_an_error + content = capture(:stderr){ run_generator(["--pretend", destination_root]) } + assert_match /Options should be given after the \w+ name. For details run: rails( plugin)? --help\n/, content + end + + def test_name_collision_raises_an_error + reserved_words = %w[application destroy plugin runner test] + reserved_words.each do |reserved| + content = capture(:stderr){ run_generator [File.join(destination_root, reserved)] } + assert_match /Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words.\n/, content + end + end + + def test_name_raises_an_error_if_name_already_used_constant + %w{ String Hash Class Module Set Symbol }.each do |ruby_class| + content = capture(:stderr){ run_generator [File.join(destination_root, ruby_class)] } + assert_match /Invalid \w+ name #{ruby_class}, constant #{ruby_class} is already in use. Please choose another \w+ name.\n/, content + end + end + + def test_shebang_is_added_to_rails_file + run_generator [destination_root, "--ruby", "foo/bar/baz"] + assert_file "script/rails", /#!foo\/bar\/baz/ + end + + def test_shebang_when_is_the_same_as_default_use_env + run_generator [destination_root, "--ruby", Thor::Util.ruby_command] + assert_file "script/rails", /#!\/usr\/bin\/env/ + end + + def test_template_raises_an_error_with_invalid_path + content = capture(:stderr){ run_generator([destination_root, "-m", "non/existant/path"]) } + assert_match /The template \[.*\] could not be loaded/, content + assert_match /non\/existant\/path/, content + end + + def test_template_is_executed_when_supplied + path = "http://gist.github.com/103208.txt" + template = %{ say "It works!" } + template.instance_eval "def read; self; end" # Make the string respond to read + + generator([destination_root], :template => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) + assert_match /It works!/, silence(:stdout){ generator.invoke_all } + end + + def test_dev_option + generator([destination_root], :dev => true).expects(:run).with("#{@bundle_command} install") + silence(:stdout){ generator.invoke_all } + rails_path = File.expand_path('../../..', Rails.root) + assert_file 'Gemfile', /^gem\s+["']rails["'],\s+:path\s+=>\s+["']#{Regexp.escape(rails_path)}["']$/ + end + + def test_edge_option + generator([destination_root], :edge => true).expects(:run).with("#{@bundle_command} install") + silence(:stdout){ generator.invoke_all } + assert_file 'Gemfile', %r{^gem\s+["']rails["'],\s+:git\s+=>\s+["']#{Regexp.escape("git://github.com/rails/rails.git")}["']$} + end + + def test_template_raises_an_error_with_invalid_path + content = capture(:stderr){ run_generator([destination_root, "-m", "non/existant/path"]) } + assert_match /The template \[.*\] could not be loaded/, content + assert_match /non\/existant\/path/, content + end + + def test_template_is_executed_when_supplied + path = "http://gist.github.com/103208.txt" + template = %{ say "It works!" } + template.instance_eval "def read; self; end" # Make the string respond to read + + generator([destination_root], :template => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) + assert_match /It works!/, silence(:stdout){ generator.invoke_all } + end + + def test_dev_option + generator([destination_root], :dev => true).expects(:run).with("#{@bundle_command} install") + silence(:stdout){ generator.invoke_all } + rails_path = File.expand_path('../../..', Rails.root) + assert_file 'Gemfile', /^gem\s+["']rails["'],\s+:path\s+=>\s+["']#{Regexp.escape(rails_path)}["']$/ + end + + def test_edge_option + generator([destination_root], :edge => true).expects(:run).with("#{@bundle_command} install") + silence(:stdout){ generator.invoke_all } + assert_file 'Gemfile', %r{^gem\s+["']rails["'],\s+:git\s+=>\s+["']#{Regexp.escape("git://github.com/rails/rails.git")}["']$} + end +end + +module SharedCustomGeneratorTests + def setup + Rails.application = TestApp::Application + super + Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) + @bundle_command = File.basename(Thor::Util.ruby_command).sub(/ruby/, 'bundle') + end + + def teardown + super + Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) + Object.class_eval do + remove_const :AppBuilder if const_defined?(:AppBuilder) + remove_const :PluginBuilder if const_defined?(:PluginBuilder) + end + Rails.application = TestApp::Application.instance + end + + def test_builder_option_with_empty_app_builder + FileUtils.cd(destination_root) + run_generator([destination_root, "-b", "#{Rails.root}/lib/#{builders_dir}/empty_builder.rb"]) + default_files.each{ |path| assert_no_file path } + end + + def test_builder_option_with_simple_plugin_builder + FileUtils.cd(destination_root) + run_generator([destination_root, "-b", "#{Rails.root}/lib/#{builders_dir}/simple_builder.rb"]) + (default_files - ['.gitignore']).each{ |path| assert_no_file path } + assert_file ".gitignore", "foobar" + end + + def test_builder_option_with_relative_path + here = File.expand_path(File.dirname(__FILE__)) + FileUtils.cd(here) + run_generator([destination_root, "-b", "../fixtures/lib/#{builders_dir}/simple_builder.rb"]) + FileUtils.cd(destination_root) + (default_files - ['.gitignore']).each{ |path| assert_no_file path } + assert_file ".gitignore", "foobar" + end + + def test_builder_option_with_tweak_plugin_builder + FileUtils.cd(destination_root) + run_generator([destination_root, "-b", "#{Rails.root}/lib/#{builders_dir}/tweak_builder.rb"]) + default_files.each{ |path| assert_file path } + assert_file ".gitignore", "foobar" + end + + def test_builder_option_with_http + path = "http://gist.github.com/103208.txt" + template = "class #{builder_class}; end" + template.instance_eval "def read; self; end" # Make the string respond to read + + generator([destination_root], :builder => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) + capture(:stdout) { generator.invoke_all } + + default_files.each{ |path| assert_no_file path } + end +end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index f93800a5ae..346c9ceb9d 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -102,6 +102,12 @@ class GeneratorsTest < Rails::Generators::TestCase assert_no_match /^ app$/, output end + def test_rails_generators_help_does_not_include_app_nor_plugin_new + output = capture(:stdout){ Rails::Generators.help } + assert_no_match /app/, output + assert_no_match /plugin_new/, output + end + def test_rails_generators_with_others_information output = capture(:stdout){ Rails::Generators.help } assert_match /Fixjour:/, output diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 4b52260ecc..7548c6318e 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -224,32 +224,14 @@ module RailtiesTest end RUBY - @plugin.write "app/views/foo/index.html.erb", <<-RUBY - <%= compute_public_path("/foo", "") %> + @plugin.write "app/views/foo/index.html.erb", <<-ERB <%= image_path("foo.png") %> <%= javascript_include_tag("foo") %> <%= stylesheet_link_tag("foo") %> - RUBY - - - app_file "app/controllers/bar_controller.rb", <<-RUBY - class BarController < ActionController::Base - def index - render :index - end - end - RUBY - - app_file "app/views/bar/index.html.erb", <<-RUBY - <%= compute_public_path("/foo", "") %> - RUBY + ERB add_to_config 'config.asset_path = "/omg%s"' - @plugin.write 'public/touch.txt', <<-RUBY - touch - RUBY - boot_rails # should set asset_path with engine name by default @@ -259,11 +241,10 @@ module RailtiesTest env = Rack::MockRequest.env_for("/foo") response = Bukkits::Engine.call(env) - stripped_body = response[2].body.split("\n").map(&:strip).join("\n") + stripped_body = response[2].body.split("\n").map(&:strip).join - expected = "/omg/bukkits/foo\n" + - "/omg/bukkits/images/foo.png\n" + - "<script src=\"/omg/bukkits/javascripts/foo.js\" type=\"text/javascript\"></script>\n" + + expected = "/omg/bukkits/images/foo.png" + + "<script src=\"/omg/bukkits/javascripts/foo.js\" type=\"text/javascript\"></script>" + "<link href=\"/omg/bukkits/stylesheets/foo.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />" assert_equal expected, stripped_body end @@ -278,21 +259,18 @@ module RailtiesTest @plugin.write "app/controllers/foo_controller.rb", <<-RUBY class FooController < ActionController::Base def index + render :inline => '<%= image_path("foo.png") %>' end end RUBY - @plugin.write "app/views/foo/index.html.erb", <<-RUBY - <%= compute_public_path("/foo", "") %> - RUBY - boot_rails env = Rack::MockRequest.env_for("/foo") response = Bukkits::Engine.call(env) stripped_body = response[2].body.strip - expected = "/bukkits/foo" + expected = "/bukkits/images/foo.png" assert_equal expected, stripped_body end @@ -685,5 +663,44 @@ module RailtiesTest assert_equal :haml , generators[:template_engine] assert_equal :rspec , generators[:test_framework] end + + test "engine should get default generators with ability to overwrite them" do + @plugin.write "lib/bukkits.rb", <<-RUBY + module Bukkits + class Engine < ::Rails::Engine + config.generators.test_framework :rspec + end + end + RUBY + + boot_rails + require "#{rails_root}/config/environment" + + generators = Bukkits::Engine.config.generators.options[:rails] + assert_equal :active_record, generators[:orm] + assert_equal :rspec , generators[:test_framework] + + app_generators = Rails.application.config.generators.options[:rails] + assert_equal :test_unit , app_generators[:test_framework] + end + + test "do not create table_name_prefix method if it already exists" do + @plugin.write "lib/bukkits.rb", <<-RUBY + module Bukkits + def self.table_name_prefix + "foo" + end + + class Engine < ::Rails::Engine + isolate_namespace(Bukkits) + end + end + RUBY + + boot_rails + require "#{rails_root}/config/environment" + + assert_equal "foo", Bukkits.table_name_prefix + end end end diff --git a/release.rb b/release.rb deleted file mode 100644 index 2076515f0e..0000000000 --- a/release.rb +++ /dev/null @@ -1,11 +0,0 @@ -version = ARGV.pop - -%w( activesupport activemodel activerecord activeresource actionpack actionmailer railties ).each do |framework| - puts "Building and pushing #{framework}..." - `cd #{framework} && gem build #{framework}.gemspec && gem push #{framework}-#{version}.gem && rm #{framework}-#{version}.gem` -end - -puts "Building and pushing Rails..." -`gem build rails.gemspec` -`gem push rails-#{version}.gem` -`rm rails-#{version}.gem`
\ No newline at end of file diff --git a/tasks/release.rb b/tasks/release.rb new file mode 100644 index 0000000000..a605fed160 --- /dev/null +++ b/tasks/release.rb @@ -0,0 +1,96 @@ +FRAMEWORKS = %w( activesupport activemodel activerecord activeresource actionpack actionmailer railties ) + +root = File.expand_path('../../', __FILE__) +version = File.read("#{root}/RAILS_VERSION").strip +tag = "v#{version}" + +directory "dist" + +(FRAMEWORKS + ['rails']).each do |framework| + namespace framework do + gem = "dist/#{framework}-#{version}.gem" + gemspec = "#{framework}.gemspec" + + task :clean do + rm_f gem + end + + task :update_version_rb do + glob = root.dup + glob << "/#{framework}/lib/*" unless framework == "rails" + glob << "/version.rb" + + file = Dir[glob].first + ruby = File.read(file) + + major, minor, tiny, pre = version.split('.') + pre = pre ? pre.inspect : "nil" + + ruby.gsub! /^(\s*)MAJOR = .*?$/, "\\1MAJOR = #{major}" + raise "Could not insert MAJOR in #{file}" unless $1 + + ruby.gsub! /^(\s*)MINOR = .*?$/, "\\1MINOR = #{minor}" + raise "Could not insert MINOR in #{file}" unless $1 + + ruby.gsub! /^(\s*)TINY = .*?$/, "\\1TINY = #{tiny}" + raise "Could not insert TINY in #{file}" unless $1 + + ruby.gsub! /^(\s*)PRE = .*?$/, "\\1PRE = #{pre}" + raise "Could not insert PRE in #{file}" unless $1 + + File.open(file, 'w') { |f| f.write ruby } + end + + task gem => %w(update_version_rb dist) do + cmd = "" + cmd << "cd #{framework} && " unless framework == "rails" + cmd << "gem build #{gemspec} && mv #{framework}-#{version}.gem #{root}/dist/" + sh cmd + end + + task :build => [:clean, gem] + task :install => :build do + sh "gem install #{gem}" + end + + task :prep_release => [:ensure_clean_state, :build] + + task :push => :build do + sh "gem push #{gem}" + end + end +end + +namespace :all do + task :build => FRAMEWORKS.map { |f| "#{f}:build" } + ['rails:build'] + task :install => FRAMEWORKS.map { |f| "#{f}:install" } + ['rails:install'] + task :push => FRAMEWORKS.map { |f| "#{f}:push" } + ['rails:push'] + + task :ensure_clean_state do + unless `git status -s | grep -v RAILS_VERSION`.strip.empty? + abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed" + end + + unless ENV['SKIP_TAG'] || `git tag | grep #{tag}`.strip.empty? + abort "[ABORTING] `git tag` shows that #{tag} already exists. Has this version already\n"\ + " been released? Git tagging can be skipped by setting SKIP_TAG=1" + end + end + + task :commit do + File.open('dist/commit_message.txt', 'w') do |f| + f.puts "# Preparing for #{version} release\n" + f.puts + f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT" + end + + sh "git add . && git commit --verbose --template=dist/commit_message.txt" + rm_f "dist/commit_message.txt" + end + + task :tag do + sh "git tag #{tag}" + end + + task :release => %w(ensure_clean_state build commit tag push) +end diff --git a/version.rb b/version.rb index 0213d46254..b076881c21 100644 --- a/version.rb +++ b/version.rb @@ -3,8 +3,8 @@ module Rails MAJOR = 3 MINOR = 1 TINY = 0 - BUILD = "beta" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end |