diff options
363 files changed, 8427 insertions, 5278 deletions
diff --git a/.travis.yml b/.travis.yml index 265124a3af..9a0ace81bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ env: - "GEM=ar:mysql" - "GEM=ar:mysql2" - "GEM=ar:sqlite3" + - "GEM=ar:postgresql" notifications: email: false irc: @@ -1,97 +1,97 @@ -source "http://rubygems.org" +source 'https://rubygems.org' gemspec if ENV['AREL'] - gem "arel", :path => ENV['AREL'] + gem 'arel', :path => ENV['AREL'] else - gem "arel", :git => "git://github.com/rails/arel" + gem 'arel' end -gem "bcrypt-ruby", "~> 3.0.0" -gem "jquery-rails" +gem 'bcrypt-ruby', '~> 3.0.0' +gem 'jquery-rails' if ENV['JOURNEY'] - gem "journey", :path => ENV['JOURNEY'] + gem 'journey', :path => ENV['JOURNEY'] else - gem "journey", :git => "git://github.com/rails/journey" + gem 'journey' end # This needs to be with require false to avoid # it being automatically loaded by sprockets -gem "uglifier", ">= 1.0.3", :require => false +gem 'uglifier', '>= 1.0.3', :require => false -gem "rake", ">= 0.8.7" -gem "mocha", ">= 0.9.8" +gem 'rake', '>= 0.8.7' +gem 'mocha', '>= 0.9.8' group :doc do - gem "rdoc", "~> 3.4" - gem "sdoc", "~> 0.3" - gem "RedCloth", "~> 4.2" if RUBY_VERSION < "1.9.3" - gem "w3c_validators" + # The current sdoc cannot generate GitHub links due + # to a bug, but the PR that fixes it has been there + # for some weeks unapplied. As a temporary solution + # this is our own fork with the fix. + gem 'sdoc', :git => 'git://github.com/fxn/sdoc.git' + gem 'RedCloth', '~> 4.2' + gem 'w3c_validators' end # AS -gem "memcache-client", ">= 1.8.5" +gem 'memcache-client', '>= 1.8.5' platforms :mri_18 do - gem "system_timer" - gem "json" + gem 'system_timer' + gem 'json' end # Add your own local bundler stuff -instance_eval File.read ".Gemfile" if File.exists? ".Gemfile" +instance_eval File.read '.Gemfile' if File.exists? '.Gemfile' platforms :mri do group :test do - gem "ruby-prof" if RUBY_VERSION < "1.9.3" + gem 'ruby-prof' if RUBY_VERSION < '1.9.3' end end platforms :ruby do - if ENV["RB_FSEVENT"] - gem "rb-fsevent" - end - gem "json" - gem "yajl-ruby" - gem "nokogiri", ">= 1.4.5" + gem 'json' + gem 'yajl-ruby' + gem 'nokogiri', '>= 1.4.5' # AR - gem "sqlite3", "~> 1.3.4" + gem 'sqlite3', '~> 1.3.5' group :db do - gem "pg", ">= 0.11.0" unless ENV['TRAVIS'] # once pg is on travis this can be removed - gem "mysql", ">= 2.8.1" - gem "mysql2", ">= 0.3.6" + gem 'pg', '>= 0.11.0' + gem 'mysql', '>= 2.8.1' + gem 'mysql2', '>= 0.3.10' end end platforms :jruby do - gem "json" - gem "activerecord-jdbcsqlite3-adapter", ">= 1.2.0" + gem 'json' + gem 'activerecord-jdbcsqlite3-adapter', '>= 1.2.0' # This is needed by now to let tests work on JRuby # TODO: When the JRuby guys merge jruby-openssl in # jruby this will be removed - gem "jruby-openssl" + gem 'jruby-openssl' group :db do - gem "activerecord-jdbcmysql-adapter", ">= 1.2.0" - gem "activerecord-jdbcpostgresql-adapter", ">= 1.2.0" + gem 'activerecord-jdbcmysql-adapter', '>= 1.2.0' + gem 'activerecord-jdbcpostgresql-adapter', '>= 1.2.0' end end # gems that are necessary for ActiveRecord tests with Oracle database if ENV['ORACLE_ENHANCED_PATH'] || ENV['ORACLE_ENHANCED'] platforms :ruby do - gem "ruby-oci8", ">= 2.0.4" + gem 'ruby-oci8', '>= 2.0.4' end if ENV['ORACLE_ENHANCED_PATH'] - gem "activerecord-oracle_enhanced-adapter", :path => ENV['ORACLE_ENHANCED_PATH'] + gem 'activerecord-oracle_enhanced-adapter', :path => ENV['ORACLE_ENHANCED_PATH'] else - gem "activerecord-oracle_enhanced-adapter", :git => "git://github.com/rsim/oracle-enhanced.git" + gem 'activerecord-oracle_enhanced-adapter', :git => 'git://github.com/rsim/oracle-enhanced.git' end end # A gem necessary for ActiveRecord tests with IBM DB -gem "ibm_db" if ENV['IBM_DB'] +gem 'ibm_db' if ENV['IBM_DB'] diff --git a/README.rdoc b/README.rdoc index 0def4e5d12..8844c74175 100644 --- a/README.rdoc +++ b/README.rdoc @@ -67,7 +67,9 @@ We encourage you to contribute to Ruby on Rails! Please check out the {Contribut guide}[http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html] for guidelines about how to proceed. {Join us}[http://contributors.rubyonrails.org]! -== Travis Build Status {<img src="https://secure.travis-ci.org/rails/rails.png"/>}[http://travis-ci.org/rails/rails] +== Build Status {<img src="https://secure.travis-ci.org/rails/rails.png"/>}[http://travis-ci.org/rails/rails] + +== Dependency Status {<img src="https://gemnasium.com/rails/rails.png?travis"/>}[https://gemnasium.com/rails/rails] == License diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc index 9f25adeccc..7bad1d01b8 100644 --- a/RELEASING_RAILS.rdoc +++ b/RELEASING_RAILS.rdoc @@ -215,7 +215,7 @@ and generates and publishes stable docs if a new stable tag is detected. The hook unfortunately is not invoked by tag pushing, so once the new stable tag has been pushed to origin, please run - curl -X POST -d '' http://rails-hooks.hashref.com/rails-master-hook + rake publish_docs You should see something like this: @@ -77,9 +77,10 @@ RDoc::Task.new do |rdoc| rdoc_main.gsub!(/^(?=\S).*?\b(?=Rails)\b/) { "#$&\\" } rdoc_main.gsub!(%r{link:/rails/rails/blob/master/(\w+)/README\.rdoc}, "link:files/\\1/README_rdoc.html") - # Remove Travis build status image from API pages. Only GitHub README page gets this image - # https build image is used to avoid GitHub caching: http://about.travis-ci.org/docs/user/status-images - rdoc_main.gsub!(%r{^== Travis.*}, '') + # Remove Travis and Gemnasium status images from API pages. Only GitHub + # README page gets these images. Travis' https build image is used to avoid + # GitHub caching: http://about.travis-ci.org/docs/user/status-images + rdoc_main.gsub!(%r{^== (Build|Dependency) Status.*}, '') File.open(RDOC_MAIN, 'w') do |f| f.write(rdoc_main) @@ -189,7 +190,7 @@ end # 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| + Net::HTTP.new('api.rubyonrails.org', 8080).start do |http| request = Net::HTTP::Post.new('/rails-master-hook') response = http.request(request) puts response.body diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 444754d0e9..5c03a29f0f 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -19,8 +19,9 @@ module ActionMailer options.stylesheets_dir ||= paths["public/stylesheets"].first # make sure readers methods get compiled - options.asset_path ||= app.config.asset_path - options.asset_host ||= app.config.asset_host + options.asset_path ||= app.config.asset_path + options.asset_host ||= app.config.asset_host + options.relative_url_root ||= app.config.relative_url_root ActiveSupport.on_load(:action_mailer) do include AbstractController::UrlFor diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index c4de029694..4e48c0260f 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -15,6 +15,12 @@ module ActionMailer include TestHelper + included do + class_attribute :_mailer_class + setup :initialize_test_deliveries + setup :set_expected_mail + end + module ClassMethods def tests(mailer) case mailer @@ -42,8 +48,6 @@ module ActionMailer end end - module InstanceMethods - protected def initialize_test_deliveries @@ -71,16 +75,8 @@ module ActionMailer def read_fixture(action) IO.readlines(File.join(Rails.root, 'test', 'fixtures', self.class.mailer_class.name.underscore, action)) end - end - - included do - class_attribute :_mailer_class - setup :initialize_test_deliveries - setup :set_expected_mail - end end include Behavior - end end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 314aa7181c..b753addef4 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,65 @@ ## Rails 3.2.0 (unreleased) ## +* The ShowExceptions middleware now accepts a exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the PATH_INFO rewritten to the status code. *José Valim* + +* Add `button_tag` support to ActionView::Helpers::FormBuilder. + + This support mimics the default behavior of `submit_tag`. + + Example: + + <%= form_for @post do |f| %> + <%= f.button %> + <% end %> + +* Date helpers accept a new option, `:use_two_digit_numbers = true`, that renders select boxes for months and days with a leading zero without changing the respective values. + For example, this is useful for displaying ISO8601-style dates such as '2011-08-01'. *Lennart Fridén and Kim Persson* + +* Make ActiveSupport::Benchmarkable a default module for ActionController::Base, so the #benchmark method is once again available in the controller context like it used to be *DHH* + +* Deprecated implied layout lookup in controllers whose parent had a explicit layout set: + + class ApplicationController + layout "application" + end + + class PostsController < ApplicationController + end + + In the example above, Posts controller will no longer automatically look up for a posts layout. + + If you need this functionality you could either remove `layout "application"` from ApplicationController or explicitly set it to nil in PostsController. *José Valim* + +* Rails will now use your default layout (such as "layouts/application") when you specify a layout with `:only` and `:except` condition, and those conditions fail. *Prem Sichanugrist* + + For example, consider this snippet: + + class CarsController + layout 'single_car', :only => :show + end + + Rails will use 'layouts/single_car' when a request comes in `:show` action, and use 'layouts/application' (or 'layouts/cars', if exists) when a request comes in for any other actions. + +* form_for with +:as+ option uses "#{action}_#{as}" as css class and id: + + Before: + + form_for(@user, :as => 'client') # => "<form class="client_new">..." + + Now: + + form_for(@user, :as => 'client') # => "<form class="new_client">..." + + *Vasiliy Ermolovich* + +* Allow rescue responses to be configured through a railtie as in `config.action_dispatch.rescue_responses`. Please look at ActiveRecord::Railtie for an example *José Valim* + +* Allow fresh_when/stale? to take a record instead of an options hash *DHH* + +* Assets should use the request protocol by default or default to relative if no request is available *Jonathan del Strother* + +* Log "Filter chain halted as CALLBACKNAME rendered or redirected" every time a before callback halts *José Valim* + * You can provide a namespace for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generate HTML id. *Vasiliy Ermolovich* @@ -10,9 +70,9 @@ <%= f.text_field :version %> <% end %> -* Refactor ActionDispatch::ShowExceptions. Controller is responsible for choice to show exceptions. *Sergey Nartimov* +* Refactor ActionDispatch::ShowExceptions. The controller is responsible for choosing to show exceptions when `consider_all_requests_local` is false. - It's possible to override +show_detailed_exceptions?+ in controllers to specify which requests should provide debugging information on errors. + It's possible to override `show_detailed_exceptions?` in controllers to specify which requests should provide debugging information on errors. The default value is now false, meaning local requests in production will no longer show the detailed exceptions page unless `show_detailed_exceptions?` is overridden and set to `request.local?`. * Responders now return 204 No Content for API requests without a response body (as in the new scaffold) *José Valim* @@ -76,6 +136,19 @@ persistent between requests so if you need to manipulate the environment for your test you need to do it before the cookie jar is created. +* ActionController::ParamsWrapper on ActiveRecord models now only wrap + attr_accessible attributes if they were set, if not, only the attributes + returned by the class method attribute_names will be wrapped. This fixes + the wrapping of nested attributes by adding them to attr_accessible. + +## Rails 3.1.4 (unreleased) ## + +* Allow to use asset_path on named_routes aliasing RailsHelper's + asset_path to path_to_asset *Adrian Pike* + +* Assets should use the request protocol by default or default to + relative if no request is available *Jonathan del Strother* + ## Rails 3.1.3 (unreleased) ## * Fix using `tranlate` helper with a html translation which uses the `:count` option for diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 41bada43ef..1aa346929e 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -23,8 +23,8 @@ Gem::Specification.new do |s| s.add_dependency('i18n', '~> 0.6') s.add_dependency('rack', '~> 1.3.5') s.add_dependency('rack-test', '~> 0.6.1') - s.add_dependency('journey', '~> 1.0.0') - s.add_dependency('sprockets', '~> 2.1.1') + s.add_dependency('journey', '~> 1.0.0.rc1') + s.add_dependency('sprockets', '~> 2.1.2') s.add_dependency('erubis', '~> 2.7.0') s.add_development_dependency('tzinfo', '~> 0.3.29') diff --git a/actionpack/examples/performance.rb b/actionpack/examples/performance.rb new file mode 100644 index 0000000000..994e745bb0 --- /dev/null +++ b/actionpack/examples/performance.rb @@ -0,0 +1,185 @@ +ENV['RAILS_ENV'] ||= 'production' + +require File.expand_path('../../../load_paths', __FILE__) +require 'action_pack' +require 'action_controller' +require 'action_view' +require 'active_model' +require 'benchmark' + +MyHash = Class.new(Hash) + +Hash.class_eval do + extend ActiveModel::Naming + include ActiveModel::Conversion +end + +class Runner + def initialize(app, output) + @app, @output = app, output + end + + def puts(*) + super if @output + end + + def call(env) + env['n'].to_i.times { @app.call(env) } + @app.call(env).tap { |response| report(env, response) } + end + + def report(env, response) + return unless ENV["DEBUG"] + out = env['rack.errors'] + out.puts response[0], response[1].to_yaml, '---' + response[2].each { |part| out.puts part } + out.puts '---' + end + + def self.puts(*) + super if @output + end + + def self.print(*) + super if @output + end + + def self.app_and_env_for(action, n) + env = Rack::MockRequest.env_for("/") + env.merge!('n' => n, 'rack.input' => StringIO.new(''), 'rack.errors' => $stdout) + app = lambda { |env| BasePostController.action(action).call(env) } + return app, env + end + + $ran = [] + + def self.run(action, n, output = true) + print "." + STDOUT.flush + @output = output + label = action.to_s + app, env = app_and_env_for(action, n) + t = Benchmark.realtime { new(app, output).call(env) } + $ran << [label, (t * 1000).to_i.to_s] if output + end + + def self.done + puts + header, content = "", "" + $ran.each do |k,v| + size = [k.size, v.size].max + 1 + header << format("%#{size}s", k) + content << format("%#{size}s", v) + end + puts header + puts content + end +end + +ActionController::Base.logger = nil +ActionController::Base.config.compile_methods! +ActionView::Resolver.caching = ENV["RAILS_ENV"] == "production" + +class BasePostController < ActionController::Base + append_view_path "#{File.dirname(__FILE__)}/views" + + def overhead + self.response_body = '' + end + + def index + render :text => '' + end + + $OBJECT = {:name => "Hello my name is omg", :address => "333 omg"} + + def partial + render :partial => "/collection", :object => $OBJECT + end + + def partial_10 + render :partial => "/ten_partials" + end + + def partial_100 + render :partial => "/hundred_partials" + end + + $COLLECTION1 = [] + 10.times do |i| + $COLLECTION1 << { :name => "Hello my name is omg", :address => "333 omg" } + end + + def coll_10 + render :partial => "/collection", :collection => $COLLECTION1 + end + + $COLLECTION2 = [] + 100.times do |i| + $COLLECTION2 << { :name => "Hello my name is omg", :address => "333 omg" } + end + + def coll_100 + render :partial => "/collection", :collection => $COLLECTION2 + end + + def uniq_100 + render :partial => $COLLECTION2 + end + + $COLLECTION3 = [] + 50.times do |i| + $COLLECTION3 << {:name => "Hello my name is omg", :address => "333 omg"} + $COLLECTION3 << MyHash.new(:name => "Hello my name is omg", :address => "333 omg") + end + + def diff_100 + render :partial => $COLLECTION3 + end + + def template_1 + render :template => "template" + end + + module Foo + def omg + "omg" + end + end + + helper Foo +end + +N = (ENV['N'] || 1000).to_i +# ActionController::Base.use_accept_header = false + +def run_all!(times, verbose) + Runner.run(:overhead, times, verbose) + Runner.run(:index, times, verbose) + Runner.run(:template_1, times, verbose) + Runner.run(:partial, times, verbose) + Runner.run(:partial_10, times, verbose) + Runner.run(:coll_10, times, verbose) + Runner.run(:partial_100, times, verbose) + Runner.run(:coll_100, times, verbose) + Runner.run(:uniq_100, times, verbose) + Runner.run(:diff_100, times, verbose) +end + +unless ENV["PROFILE"] + run_all!(1, false) + + (ENV["M"] || 1).to_i.times do + $ran = [] + run_all!(N, true) + Runner.done + end +else + Runner.run(ENV["PROFILE"].to_sym, 1, false) + require "ruby-prof" + RubyProf.start + Runner.run(ENV["PROFILE"].to_sym, N, true) + result = RubyProf.stop + printer = RubyProf::CallStackPrinter.new(result) + printer.print(File.open("output.html", "w")) +end
\ No newline at end of file diff --git a/actionpack/examples/views/_collection.erb b/actionpack/examples/views/_collection.erb new file mode 100644 index 0000000000..cee3fe64c0 --- /dev/null +++ b/actionpack/examples/views/_collection.erb @@ -0,0 +1,3 @@ +<%= collection[:name] %> +<%= collection[:address] %> +<%= omg %>
\ No newline at end of file diff --git a/actionpack/examples/views/_hello.erb b/actionpack/examples/views/_hello.erb new file mode 100644 index 0000000000..5ab2f8a432 --- /dev/null +++ b/actionpack/examples/views/_hello.erb @@ -0,0 +1 @@ +Hello
\ No newline at end of file diff --git a/actionpack/examples/views/_hundred_partials.erb b/actionpack/examples/views/_hundred_partials.erb new file mode 100644 index 0000000000..35c2a6c9d3 --- /dev/null +++ b/actionpack/examples/views/_hundred_partials.erb @@ -0,0 +1,3 @@ +<% 100.times do %> + <%= render :partial => "/collection", :object => $OBJECT %> +<% end %>
\ No newline at end of file diff --git a/actionpack/examples/views/_partial.erb b/actionpack/examples/views/_partial.erb new file mode 100644 index 0000000000..3ca8e80b52 --- /dev/null +++ b/actionpack/examples/views/_partial.erb @@ -0,0 +1,10 @@ +<%= "Hello" %> +<%= "Hello" %> +<%= "Hello" %> +<%= "Hello" %> +<%= "Hello" %> +<%= "Hello" %> +<%= "Hello" %> +<%= "Hello" %> +<%= "Hello" %> +<%= "Hello" %> diff --git a/actionpack/examples/views/_ten_partials.erb b/actionpack/examples/views/_ten_partials.erb new file mode 100644 index 0000000000..fd02991e22 --- /dev/null +++ b/actionpack/examples/views/_ten_partials.erb @@ -0,0 +1,10 @@ +<%= render :partial => '/collection', :object => $OBJECT %> +<%= render :partial => '/collection', :object => $OBJECT %> +<%= render :partial => '/collection', :object => $OBJECT %> +<%= render :partial => '/collection', :object => $OBJECT %> +<%= render :partial => '/collection', :object => $OBJECT %> +<%= render :partial => '/collection', :object => $OBJECT %> +<%= render :partial => '/collection', :object => $OBJECT %> +<%= render :partial => '/collection', :object => $OBJECT %> +<%= render :partial => '/collection', :object => $OBJECT %> +<%= render :partial => '/collection', :object => $OBJECT %>
\ No newline at end of file diff --git a/actionpack/examples/views/hashes/_hash.erb b/actionpack/examples/views/hashes/_hash.erb new file mode 100644 index 0000000000..c100a290bd --- /dev/null +++ b/actionpack/examples/views/hashes/_hash.erb @@ -0,0 +1,3 @@ +<%= hash[:name] %> +<%= hash[:address] %> +<%= omg %>
\ No newline at end of file diff --git a/actionpack/examples/views/my_hashes/_my_hash.erb b/actionpack/examples/views/my_hashes/_my_hash.erb new file mode 100644 index 0000000000..e25d84101a --- /dev/null +++ b/actionpack/examples/views/my_hashes/_my_hash.erb @@ -0,0 +1,3 @@ +<%= my_hash[:name] %> +<%= my_hash[:address] %> +<%= omg %>
\ No newline at end of file diff --git a/actionpack/examples/views/template.html.erb b/actionpack/examples/views/template.html.erb new file mode 100644 index 0000000000..5ab2f8a432 --- /dev/null +++ b/actionpack/examples/views/template.html.erb @@ -0,0 +1 @@ +Hello
\ No newline at end of file diff --git a/actionpack/lib/abstract_controller/asset_paths.rb b/actionpack/lib/abstract_controller/asset_paths.rb index c2a6809f58..dd5f9a1942 100644 --- a/actionpack/lib/abstract_controller/asset_paths.rb +++ b/actionpack/lib/abstract_controller/asset_paths.rb @@ -4,7 +4,7 @@ module AbstractController included do config_accessor :asset_host, :asset_path, :assets_dir, :javascripts_dir, - :stylesheets_dir, :default_asset_host_protocol + :stylesheets_dir, :default_asset_host_protocol, :relative_url_root end end end diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index bbf5efe565..8b6136d6ba 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -66,27 +66,40 @@ module AbstractController # == Inheritance Examples # # class BankController < ActionController::Base - # layout "bank_standard" + # # bank.html.erb exists + # + # class ExchangeController < BankController + # # exchange.html.erb exists + # + # class CurrencyController < BankController # # class InformationController < BankController + # layout "information" # - # class TellerController < BankController + # class TellerController < InformationController # # teller.html.erb exists # - # class TillController < TellerController + # class EmployeeController < InformationController + # # employee.html.erb exists + # layout nil # # class VaultController < BankController # layout :access_level_layout # - # class EmployeeController < BankController - # layout nil + # class TillController < BankController + # layout false + # + # In these examples, we have three implicit lookup scenrios: + # * The BankController uses the "bank" layout. + # * The ExchangeController uses the "exchange" layout. + # * The CurrencyController inherits the layout from BankController. # - # In these examples: - # * The InformationController uses the "bank_standard" layout, inherited from BankController. - # * The TellerController follows convention and uses +app/views/layouts/teller.html.erb+. - # * The TillController inherits the layout from TellerController and uses +teller.html.erb+ as well. + # However, when a layout is explicitly set, the explicitly set layout wins: + # * The InformationController uses the "information" layout, explicitly set. + # * The TellerController also uses the "information" layout, because the parent explicitly set it. + # * The EmployeeController uses the "employee" layout, because it set the layout to nil, resetting the parent configuration. # * The VaultController chooses a layout dynamically by calling the <tt>access_level_layout</tt> method. - # * The EmployeeController does not use a layout at all. + # * The TillController does not use a layout at all. # # == Types of layouts # @@ -126,6 +139,22 @@ module AbstractController # If no directory is specified for the template name, the template will by default be looked for in <tt>app/views/layouts/</tt>. # Otherwise, it will be looked up relative to the template root. # + # Setting the layout to nil forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists. + # Setting it to nil is useful to re-enable template lookup overriding a previous configuration set in the parent: + # + # class ApplicationController < ActionController::Base + # layout "application" + # end + # + # class PostsController < ApplicationController + # # Will use "application" layout + # end + # + # class CommentsController < ApplicationController + # # Will search for "comments" layout and fallback "application" layout + # layout nil + # end + # # == Conditional layouts # # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering @@ -168,11 +197,12 @@ module AbstractController included do class_attribute :_layout_conditions remove_possible_method :_layout_conditions - delegate :_layout_conditions, :to => :'self.class' self._layout_conditions = {} _write_layout_method end + delegate :_layout_conditions, :to => "self.class" + module ClassMethods def inherited(klass) super @@ -188,7 +218,7 @@ module AbstractController # # ==== Returns # * <tt> Boolean</tt> - True if the action has a layout, false otherwise. - def action_has_layout? + def conditional_layout? return unless super conditions = _layout_conditions @@ -244,43 +274,71 @@ module AbstractController def _write_layout_method remove_possible_method(:_layout) - case defined?(@_layout) ? @_layout : nil - when String - self.class_eval %{def _layout; #{@_layout.inspect} end}, __FILE__, __LINE__ - when Symbol - self.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1 - def _layout - #{@_layout}.tap do |layout| - unless layout.is_a?(String) || !layout - raise ArgumentError, "Your layout method :#{@_layout} returned \#{layout}. It " \ - "should have returned a String, false, or nil" + prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"] + name_clause = if name + <<-RUBY + lookup_context.find_all("#{_implied_layout_name}", #{prefixes.inspect}).first || super + RUBY + end + + if defined?(@_layout) + layout_definition = case @_layout + when String + @_layout.inspect + when Symbol + <<-RUBY + #{@_layout}.tap do |layout| + unless layout.is_a?(String) || !layout + raise ArgumentError, "Your layout method :#{@_layout} returned \#{layout}. It " \ + "should have returned a String, false, or nil" + end end - end + RUBY + when Proc + define_method :_layout_from_proc, &@_layout + "_layout_from_proc(self)" + when false + nil + when true + raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil" + when nil + name_clause end - ruby_eval - when Proc - define_method :_layout_from_proc, &@_layout - self.class_eval %{def _layout; _layout_from_proc(self) end}, __FILE__, __LINE__ - when false - self.class_eval %{def _layout; end}, __FILE__, __LINE__ - when true - raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil" - when nil - if name - _prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"] + else + # Add a deprecation if the parent layout was explicitly set and the child + # still does a dynamic lookup. In next Rails release, we should @_layout + # to be inheritable so we can skip the child lookup if the parent explicitly + # set the layout. + parent = self.superclass.instance_variable_get(:@_layout) + @_layout = nil + inspect = parent.is_a?(Proc) ? parent.inspect : parent - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout - if template_exists?("#{_implied_layout_name}", #{_prefixes.inspect}) - "#{_implied_layout_name}" + layout_definition = if parent.nil? + name_clause + elsif name + <<-RUBY + if template = lookup_context.find_all("#{_implied_layout_name}", #{prefixes.inspect}).first + ActiveSupport::Deprecation.warn 'Layout found at "#{_implied_layout_name}" for #{name} but parent controller ' \ + 'set layout to #{inspect.inspect}. Please explicitly set your layout to "#{_implied_layout_name}" ' \ + 'or set it to nil to force a dynamic lookup.' + template else super end - end - RUBY - end + RUBY + end end - self.class_eval { private :_layout } + + self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _layout + if conditional_layout? + #{layout_definition} + else + #{name_clause} + end + end + private :_layout + RUBY end end @@ -289,8 +347,7 @@ module AbstractController if _include_layout?(options) layout = options.key?(:layout) ? options.delete(:layout) : :default - value = _layout_for_option(layout) - options[:layout] = (value =~ /\blayouts/ ? value : "layouts/#{value}") if value + options[:layout] = _layout_for_option(layout) end end @@ -305,6 +362,10 @@ module AbstractController @_action_has_layout end + def conditional_layout? + true + end + private # This will be overwritten by _write_layout_method @@ -316,9 +377,10 @@ module AbstractController # * <tt>name</tt> - The name of the template def _layout_for_option(name) case name - when String then name - when true then _default_layout(true) - when :default then _default_layout(false) + when String then _normalize_layout(name) + when Proc then name + when true then Proc.new { _default_layout(true) } + when :default then Proc.new { _default_layout(false) } when false, nil then nil else raise ArgumentError, @@ -326,6 +388,10 @@ module AbstractController end end + def _normalize_layout(value) + value.is_a?(String) && value !~ /\blayouts/ ? "layouts/#{value}" : value + end + # Returns the default layout for this controller. # Optionally raises an exception if the layout could not be found. # @@ -337,17 +403,17 @@ module AbstractController # * <tt>template</tt> - The template object for the default layout (or nil) def _default_layout(require_layout = false) begin - layout_name = _layout if action_has_layout? + value = _layout if action_has_layout? rescue NameError => e raise e, "Could not render layout: #{e.message}" end - if require_layout && action_has_layout? && !layout_name + if require_layout && action_has_layout? && !value raise ArgumentError, "There was no default layout for #{self.class} in #{view_paths.inspect}" end - layout_name + _normalize_layout(value) end def _include_layout?(options) diff --git a/actionpack/lib/abstract_controller/logger.rb b/actionpack/lib/abstract_controller/logger.rb index 0b196119f4..8b36230397 100644 --- a/actionpack/lib/abstract_controller/logger.rb +++ b/actionpack/lib/abstract_controller/logger.rb @@ -7,7 +7,7 @@ module AbstractController included do config_accessor :logger - extend ActiveSupport::Benchmarkable + include ActiveSupport::Benchmarkable end end end diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 41fdc11196..d95770c049 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -66,12 +66,7 @@ module AbstractController end def view_context_class - @_view_context_class || self.class.view_context_class - end - - def initialize(*) - @_view_context_class = nil - super + @_view_context_class ||= self.class.view_context_class end # An instance of a view class. The default view class is ActionView::Base diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionpack/lib/abstract_controller/view_paths.rb index e8394447a7..96118b940f 100644 --- a/actionpack/lib/abstract_controller/view_paths.rb +++ b/actionpack/lib/abstract_controller/view_paths.rb @@ -10,7 +10,7 @@ module AbstractController self._view_paths.freeze end - delegate :find_template, :template_exists?, :view_paths, :formats, :formats=, + delegate :template_exists?, :view_paths, :formats, :formats=, :locale, :locale=, :to => :lookup_context module ClassMethods diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index f988de39dd..3b86a9a93a 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -116,9 +116,8 @@ module ActionController #:nodoc: def expire_action(options = {}) return unless cache_configured? - actions = options[:action] - if actions.is_a?(Array) - actions.each {|action| expire_action(options.merge(:action => action)) } + if options.is_a?(Hash) && options[:action].is_a?(Array) + options[:action].each {|action| expire_action(options.merge(:action => action)) } else expire_fragment(ActionCachePath.new(self, options, false).path) end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 35e29398e6..4c76f4c43b 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -20,15 +20,18 @@ module ActionController status = payload[:status] if status.nil? && payload[:exception].present? - status = Rack::Utils.status_code(ActionDispatch::ShowExceptions.rescue_responses[payload[:exception].first]) rescue nil + status = Rack::Utils.status_code(ActionDispatch::ExceptionWrapper.new({}, payload[:exception]).status_code) end message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in %.0fms" % event.duration message << " (#{additions.join(" | ")})" unless additions.blank? - message << "\n" info(message) end + def halted_callback(event) + info "Filter chain halted as #{event.payload[:filter]} rendered or redirected" + end + def send_file(event) message = "Sent file %s" message << " (%.1fms)" diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index a5e37172c9..1645400693 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -23,8 +23,27 @@ module ActionController # This will render the show template if the request isn't sending a matching etag or # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match. # - def fresh_when(options) - options.assert_valid_keys(:etag, :last_modified, :public) + # You can also just pass a record where last_modified will be set by calling updated_at and the etag by passing the object itself. Example: + # + # def show + # @article = Article.find(params[:id]) + # fresh_when(@article) + # end + # + # When passing a record, you can still set whether the public header: + # + # def show + # @article = Article.find(params[:id]) + # fresh_when(@article, :public => true) + # end + def fresh_when(record_or_options, additional_options = {}) + if record_or_options.is_a? Hash + options = record_or_options + options.assert_valid_keys(:etag, :last_modified, :public) + else + record = record_or_options + options = { :etag => record, :last_modified => record.try(:updated_at) }.merge(additional_options) + end response.etag = options[:etag] if options[:etag] response.last_modified = options[:last_modified] if options[:last_modified] @@ -55,8 +74,34 @@ module ActionController # end # end # end - def stale?(options) - fresh_when(options) + # + # You can also just pass a record where last_modified will be set by calling updated_at and the etag by passing the object itself. Example: + # + # def show + # @article = Article.find(params[:id]) + # + # if stale?(@article) + # @statistics = @article.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end + # end + # end + # + # When passing a record, you can still set whether the public header: + # + # def show + # @article = Article.find(params[:id]) + # + # if stale?(@article, :public => true) + # @statistics = @article.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end + # end + # end + def stale?(record_or_options, additional_options = {}) + fresh_when(record_or_options, additional_options) !request.fresh?(response) end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 777a0ab343..640ebf5f00 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -64,7 +64,12 @@ module ActionController end end - protected + private + + # A hook invoked everytime a before callback is halted. + def halted_callback_hook(filter) + ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter) + end # A hook which allows you to clean up any time taken into account in # views wrongly, like database querying time. diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index e0d8e1c992..5c28a8074f 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -43,6 +43,11 @@ module ActionController # wrap_parameters :person, :include => [:username, :password] # end # + # On ActiveRecord models with no +:include+ or +:exclude+ option set, + # if attr_accessible is set on that model, it will only wrap the accessible + # parameters, else it will only wrap the parameters returned by the class + # method attribute_names. + # # If you're going to pass the parameters to an +ActiveModel+ object (such as # +User.new(params[:user])+), you might consider passing the model class to # the method instead. The +ParamsWrapper+ will actually try to determine the @@ -141,7 +146,7 @@ module ActionController # try to find Foo::Bar::User, Foo::User and finally User. def _default_wrap_model #:nodoc: return nil if self.anonymous? - model_name = self.name.sub(/Controller$/, '').singularize + model_name = self.name.sub(/Controller$/, '').classify begin if model_klass = model_name.safe_constantize @@ -162,7 +167,9 @@ module ActionController unless options[:include] || options[:exclude] model ||= _default_wrap_model - if model.respond_to?(:attribute_names) && model.attribute_names.present? + if model.respond_to?(:accessible_attributes) && model.accessible_attributes.present? + options[:include] = model.accessible_attributes.to_a + elsif model.respond_to?(:attribute_names) && model.attribute_names.present? options[:include] = model.attribute_names end end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 0355c9f458..b07742e0e1 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -18,7 +18,7 @@ module ActionController # # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+. # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record. - # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) - Is passed straight through as the target for redirection. + # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection. # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string. # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+. # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. @@ -81,7 +81,8 @@ module ActionController # The scheme name consist of a letter followed by any combination of # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") # characters; and is terminated by a colon (":"). - when %r{^\w[\w+.-]*:.*} + # The protocol relative scheme starts with a double slash "//" + when %r{^(\w[\w+.-]*:|//).*} options when String request.protocol + request.host_with_port + options diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb index 736ff5b31c..68cc9a9c9b 100644 --- a/actionpack/lib/action_controller/metal/rescue.rb +++ b/actionpack/lib/action_controller/metal/rescue.rb @@ -1,13 +1,11 @@ module ActionController #:nodoc: + # This module is responsible to provide `rescue_from` helpers + # to controllers and configure when detailed exceptions must be + # shown. module Rescue extend ActiveSupport::Concern include ActiveSupport::Rescuable - included do - config_accessor :consider_all_requests_local - self.consider_all_requests_local = false if consider_all_requests_local.nil? - end - def rescue_with_handler(exception) if (exception.respond_to?(:original_exception) && (orig_exception = exception.original_exception) && @@ -17,15 +15,20 @@ module ActionController #:nodoc: super(exception) end + # Override this method if you want to customize when detailed + # exceptions must be shown. This method is only called when + # consider_all_requests_local is false. By default, it returns + # false, but someone may set it to `request.local?` so local + # requests in production still shows the detailed exception pages. def show_detailed_exceptions? - consider_all_requests_local || request.local? + false end private def process_action(*args) super rescue Exception => exception - request.env['action_dispatch.show_detailed_exceptions'] = show_detailed_exceptions? + request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions? rescue_with_handler(exception) || raise(exception) end end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index de7b837ecc..7af256fd99 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -21,8 +21,6 @@ module ActionController paths = app.config.paths options = app.config.action_controller - options.consider_all_requests_local ||= app.config.consider_all_requests_local - options.assets_dir ||= paths["public"].first options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first @@ -31,6 +29,7 @@ module ActionController # make sure readers methods get compiled options.asset_path ||= app.config.asset_path options.asset_host ||= app.config.asset_host + options.relative_url_root ||= app.config.relative_url_root ActiveSupport.on_load(:action_controller) do include app.routes.mounted_helpers diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 6913c1ef4a..7470897659 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -86,6 +86,21 @@ module ActionController end end when Hash + if expected_layout = options[:layout] + msg = build_message(message, + "expecting layout <?> but action rendered <?>", + expected_layout, @layouts.keys) + + case expected_layout + when String + assert(@layouts.keys.include?(expected_layout), msg) + when Regexp + assert(@layouts.keys.any? {|l| l =~ expected_layout }, msg) + when nil + assert(@layouts.empty?, msg) + end + end + if expected_partial = options[:partial] if expected_locals = options[:locals] actual_locals = @locals[expected_partial.to_s.sub(/^_/,'')] @@ -98,19 +113,6 @@ module ActionController "expecting ? to be rendered ? time(s) but rendered ? time(s)", expected_partial, expected_count, actual_count) assert(actual_count == expected_count.to_i, msg) - elsif options.key?(:layout) - msg = build_message(message, - "expecting layout <?> but action rendered <?>", - expected_layout, @layouts.keys) - - case layout = options[:layout] - when String - assert(@layouts.include?(expected_layout), msg) - when Regexp - assert(@layouts.any? {|l| l =~ layout }, msg) - when nil - assert(@layouts.empty?, msg) - end else msg = build_message(message, "expecting partial <?> but action rendered <?>", @@ -296,11 +298,11 @@ module ActionController # assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave" # assert flash.empty? # makes sure that there's nothing in the flash # - # For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To + # For historic reasons, the assigns hash uses string-based keys. So <tt>assigns[:person]</tt> won't work, but <tt>assigns["person"]</tt> will. To # appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing. - # So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work. + # So <tt>assigns(:person)</tt> will work just like <tt>assigns["person"]</tt>, but again, <tt>assigns[:person]</tt> will not work. # - # On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url. + # On top of the collections, you have the complete url that a given action redirected to available in <tt>redirect_to_url</tt>. # # For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another # action call which can then be asserted against. diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 1e92d14542..4b821037dc 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -51,9 +51,12 @@ module ActionDispatch autoload :BestStandardsSupport autoload :Callbacks autoload :Cookies + autoload :DebugExceptions + autoload :ExceptionWrapper autoload :Flash autoload :Head autoload :ParamsParser + autoload :PublicExceptions autoload :Reloader autoload :RemoteIp autoload :Rescue @@ -61,7 +64,6 @@ module ActionDispatch autoload :Static end - autoload :ClosedError, 'action_dispatch/middleware/closed_error' autoload :MiddlewareStack, 'action_dispatch/middleware/stack' autoload :Routing diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index aaed0d750f..bea62b94d2 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -4,14 +4,18 @@ module ActionDispatch module Http module Cache module Request + + HTTP_IF_MODIFIED_SINCE = 'HTTP_IF_MODIFIED_SINCE'.freeze + HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze + def if_modified_since - if since = env['HTTP_IF_MODIFIED_SINCE'] + if since = env[HTTP_IF_MODIFIED_SINCE] Time.rfc2822(since) rescue nil end end def if_none_match - env['HTTP_IF_NONE_MATCH'] + env[HTTP_IF_NONE_MATCH] end def not_modified?(modified_at) @@ -43,31 +47,35 @@ module ActionDispatch alias :etag? :etag def last_modified - if last = headers['Last-Modified'] + if last = headers[LAST_MODIFIED] Time.httpdate(last) end end def last_modified? - headers.include?('Last-Modified') + headers.include?(LAST_MODIFIED) end def last_modified=(utc_time) - headers['Last-Modified'] = utc_time.httpdate + headers[LAST_MODIFIED] = utc_time.httpdate end def etag=(etag) key = ActiveSupport::Cache.expand_cache_key(etag) - @etag = self["ETag"] = %("#{Digest::MD5.hexdigest(key)}") + @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}") end private + LAST_MODIFIED = "Last-Modified".freeze + ETAG = "ETag".freeze + CACHE_CONTROL = "Cache-Control".freeze + def prepare_cache_control! @cache_control = {} - @etag = self["ETag"] + @etag = self[ETAG] - if cache_control = self["Cache-Control"] + if cache_control = self[CACHE_CONTROL] cache_control.split(/,\s*/).each do |segment| first, last = segment.split("=") @cache_control[first.to_sym] = last || true @@ -81,28 +89,32 @@ module ActionDispatch end end - DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" + DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze + NO_CACHE = "no-cache".freeze + PUBLIC = "public".freeze + PRIVATE = "private".freeze + MUST_REVALIDATE = "must-revalidate".freeze def set_conditional_cache_control! - return if self["Cache-Control"].present? + return if self[CACHE_CONTROL].present? control = @cache_control if control.empty? - headers["Cache-Control"] = DEFAULT_CACHE_CONTROL + headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL elsif control[:no_cache] - headers["Cache-Control"] = "no-cache" + headers[CACHE_CONTROL] = NO_CACHE else extras = control[:extras] max_age = control[:max_age] options = [] options << "max-age=#{max_age.to_i}" if max_age - options << (control[:public] ? "public" : "private") - options << "must-revalidate" if control[:must_revalidate] + options << (control[:public] ? PUBLIC : PRIVATE) + options << MUST_REVALIDATE if control[:must_revalidate] options.concat(extras) if extras - headers["Cache-Control"] = options.join(", ") + headers[CACHE_CONTROL] = options.join(", ") end end end diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index 1480e8f77c..490b46c990 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -20,6 +20,8 @@ module ActionDispatch @filters.present? end + FILTERED = '[FILTERED]'.freeze + def compiled_filter @compiled_filter ||= begin regexps, blocks = compile_filter @@ -29,7 +31,7 @@ module ActionDispatch original_params.each do |key, value| if regexps.find { |r| key =~ r } - value = '[FILTERED]' + value = FILTERED elsif value.is_a?(Hash) value = filter(value) elsif value.is_a?(Array) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 69ca050d0c..c5c48ec489 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -35,14 +35,6 @@ module ActionDispatch METHOD end - def self.new(env) - if request = env["action_dispatch.request"] && request.instance_of?(self) - return request - end - - super - end - def key?(key) @env.key?(key) end @@ -94,31 +86,31 @@ module ActionDispatch end # Is this a GET (or HEAD) request? - # Equivalent to <tt>request.request_method == :get</tt>. + # Equivalent to <tt>request.request_method_symbol == :get</tt>. def get? HTTP_METHOD_LOOKUP[request_method] == :get end # Is this a POST request? - # Equivalent to <tt>request.request_method == :post</tt>. + # Equivalent to <tt>request.request_method_symbol == :post</tt>. def post? HTTP_METHOD_LOOKUP[request_method] == :post end # Is this a PUT request? - # Equivalent to <tt>request.request_method == :put</tt>. + # Equivalent to <tt>request.request_method_symbol == :put</tt>. def put? HTTP_METHOD_LOOKUP[request_method] == :put end # Is this a DELETE request? - # Equivalent to <tt>request.request_method == :delete</tt>. + # Equivalent to <tt>request.request_method_symbol == :delete</tt>. def delete? HTTP_METHOD_LOOKUP[request_method] == :delete end # Is this a HEAD request? - # Equivalent to <tt>request.method == :head</tt>. + # Equivalent to <tt>request.method_symbol == :head</tt>. def head? HTTP_METHOD_LOOKUP[method] == :head end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index f1e85559a3..5797c63924 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -53,8 +53,10 @@ module ActionDispatch # :nodoc: # information. attr_accessor :charset, :content_type - CONTENT_TYPE = "Content-Type" - + CONTENT_TYPE = "Content-Type".freeze + SET_COOKIE = "Set-Cookie".freeze + LOCATION = "Location".freeze + cattr_accessor(:default_charset) { "utf-8" } include Rack::Response::Helpers @@ -66,10 +68,10 @@ module ActionDispatch # :nodoc: @sending_file = false @blank = false - if content_type = self["Content-Type"] + if content_type = self[CONTENT_TYPE] type, charset = content_type.split(/;\s*charset=/) @content_type = Mime::Type.lookup(type) - @charset = charset || "UTF-8" + @charset = charset || self.class.default_charset end prepare_cache_control! @@ -109,9 +111,9 @@ module ActionDispatch # :nodoc: end def body - str = '' - each { |part| str << part.to_s } - str + strings = [] + each { |part| strings << part.to_s } + strings.join end EMPTY = " " @@ -142,12 +144,12 @@ module ActionDispatch # :nodoc: end def location - headers['Location'] + headers[LOCATION] end alias_method :redirect_url, :location def location=(url) - headers['Location'] = url + headers[LOCATION] = url end def close @@ -158,10 +160,10 @@ module ActionDispatch # :nodoc: assign_default_content_type_and_charset! handle_conditional_get! - @header["Set-Cookie"] = @header["Set-Cookie"].join("\n") if @header["Set-Cookie"].respond_to?(:join) + @header[SET_COOKIE] = @header[SET_COOKIE].join("\n") if @header[SET_COOKIE].respond_to?(:join) if [204, 304].include?(@status) - @header.delete "Content-Type" + @header.delete CONTENT_TYPE [@status, @header, []] else [@status, @header, self] @@ -175,7 +177,7 @@ module ActionDispatch # :nodoc: # assert_equal 'AuthorOfNewPage', r.cookies['author'] def cookies cookies = {} - if header = self["Set-Cookie"] + if header = self[SET_COOKIE] header = header.split("\n") if header.respond_to?(:to_str) header.each do |cookie| if pair = cookie.split(';').first diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 129a8b1031..64459836b5 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -70,7 +70,7 @@ module ActionDispatch host = "" unless options[:subdomain] == false - host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)) + host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)).to_param host << "." end host << (options[:domain] || extract_domain(options[:host], tld_length)) diff --git a/actionpack/lib/action_dispatch/middleware/closed_error.rb b/actionpack/lib/action_dispatch/middleware/closed_error.rb deleted file mode 100644 index 0a4db47f4b..0000000000 --- a/actionpack/lib/action_dispatch/middleware/closed_error.rb +++ /dev/null @@ -1,7 +0,0 @@ -module ActionDispatch - class ClosedError < StandardError #:nodoc: - def initialize(kind) - super "Cannot modify #{kind} because it was closed. This means it was already streamed back to the client or converted to HTTP headers." - end - end -end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 51cec41a34..39ff58a447 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -121,10 +121,6 @@ module ActionDispatch @cookies = {} end - attr_reader :closed - alias :closed? :closed - def close!; @closed = true end - def each(&block) @cookies.each(&block) end @@ -165,7 +161,6 @@ module ActionDispatch # Sets the cookie named +name+. The second argument may be the very cookie # value, or a hash of options as documented above. def []=(key, options) - raise ClosedError, :cookies if closed? if options.is_a?(Hash) options.symbolize_keys! value = options[:value] @@ -259,7 +254,6 @@ module ActionDispatch end def []=(key, options) - raise ClosedError, :cookies if closed? if options.is_a?(Hash) options.symbolize_keys! else @@ -298,7 +292,6 @@ module ActionDispatch end def []=(key, options) - raise ClosedError, :cookies if closed? if options.is_a?(Hash) options.symbolize_keys! options[:value] = @verifier.generate(options[:value]) @@ -352,9 +345,6 @@ module ActionDispatch end [status, headers, body] - ensure - cookie_jar = ActionDispatch::Request.new(env).cookie_jar unless cookie_jar - cookie_jar.close! end end end diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb new file mode 100644 index 0000000000..dabbabb1e6 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -0,0 +1,82 @@ +require 'action_dispatch/http/request' +require 'action_dispatch/middleware/exception_wrapper' + +module ActionDispatch + # This middleware is responsible for logging exceptions and + # showing a debugging page in case the request is local. + class DebugExceptions + RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') + + def initialize(app) + @app = app + end + + def call(env) + begin + response = @app.call(env) + + if response[1]['X-Cascade'] == 'pass' + body = response[2] + body.close if body.respond_to?(:close) + raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}" + end + rescue Exception => exception + raise exception if env['action_dispatch.show_exceptions'] == false + end + + exception ? render_exception(env, exception) : response + end + + private + + def render_exception(env, exception) + wrapper = ExceptionWrapper.new(env, exception) + log_error(env, wrapper) + + if env['action_dispatch.show_detailed_exceptions'] + template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], + :request => Request.new(env), + :exception => wrapper.exception, + :application_trace => wrapper.application_trace, + :framework_trace => wrapper.framework_trace, + :full_trace => wrapper.full_trace + ) + + file = "rescues/#{wrapper.rescue_template}" + body = template.render(:template => file, :layout => 'rescues/layout') + render(wrapper.status_code, body) + else + raise exception + end + end + + def render(status, body) + [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] + end + + def log_error(env, wrapper) + logger = logger(env) + return unless logger + + exception = wrapper.exception + + trace = wrapper.application_trace + trace = wrapper.framework_trace if trace.empty? + + ActiveSupport::Deprecation.silence do + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << trace.join("\n ") + logger.fatal("#{message}\n\n") + end + end + + def logger(env) + env['action_dispatch.logger'] || stderr_logger + end + + def stderr_logger + @stderr_logger ||= Logger.new($stderr) + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb new file mode 100644 index 0000000000..c0532c80c4 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -0,0 +1,78 @@ +require 'action_controller/metal/exceptions' +require 'active_support/core_ext/exception' + +module ActionDispatch + class ExceptionWrapper + cattr_accessor :rescue_responses + @@rescue_responses = Hash.new(:internal_server_error) + @@rescue_responses.merge!( + 'ActionController::RoutingError' => :not_found, + 'AbstractController::ActionNotFound' => :not_found, + 'ActionController::MethodNotAllowed' => :method_not_allowed, + 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity + ) + + cattr_accessor :rescue_templates + @@rescue_templates = Hash.new('diagnostics') + @@rescue_templates.merge!( + 'ActionView::MissingTemplate' => 'missing_template', + 'ActionController::RoutingError' => 'routing_error', + 'AbstractController::ActionNotFound' => 'unknown_action', + 'ActionView::Template::Error' => 'template_error' + ) + + attr_reader :env, :exception + + def initialize(env, exception) + @env = env + @exception = original_exception(exception) + end + + def rescue_template + @@rescue_templates[@exception.class.name] + end + + def status_code + Rack::Utils.status_code(@@rescue_responses[@exception.class.name]) + end + + def application_trace + clean_backtrace(:silent) + end + + def framework_trace + clean_backtrace(:noise) + end + + def full_trace + clean_backtrace(:all) + end + + private + + def original_exception(exception) + if registered_original_exception?(exception) + exception.original_exception + else + exception + end + end + + def registered_original_exception?(exception) + exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name) + end + + def clean_backtrace(*args) + if backtrace_cleaner + backtrace_cleaner.clean(@exception.backtrace, *args) + else + @exception.backtrace + end + end + + def backtrace_cleaner + @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner'] + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index e59404ef68..bc5b163931 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -93,7 +93,6 @@ module ActionDispatch end def []=(k, v) #:nodoc: - raise ClosedError, :flash if closed? keep(k) @flashes[k] = v end @@ -159,10 +158,6 @@ module ActionDispatch @now ||= FlashNow.new(self) end - attr_reader :closed - alias :closed? :closed - def close!; @closed = true; end - # Keeps either the entire current flash or a specific flash entry available for the next action: # # flash.keep # keeps the entire flash @@ -258,7 +253,6 @@ module ActionDispatch end env[KEY] = new_hash - new_hash.close! end if session.key?('flash') && session['flash'].empty? diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb new file mode 100644 index 0000000000..85b8d178bf --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -0,0 +1,30 @@ +module ActionDispatch + # A simple Rack application that renders exceptions in the given public path. + class PublicExceptions + attr_accessor :public_path + + def initialize(public_path) + @public_path = public_path + end + + def call(env) + status = env["PATH_INFO"][1..-1] + locale_path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale + path = "#{public_path}/#{status}.html" + + if locale_path && File.exist?(locale_path) + render(status, File.read(locale_path)) + elsif File.exist?(path) + render(status, File.read(path)) + else + [404, { "X-Cascade" => "pass" }, []] + end + end + + private + + def render(status, body) + [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 29289a76b4..4f48f1c974 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -43,34 +43,58 @@ module ActionDispatch # Execute all prepare callbacks. def self.prepare! - new(nil).run_callbacks :prepare + new(nil).prepare! end # Execute all cleanup callbacks. def self.cleanup! - new(nil).run_callbacks :cleanup + new(nil).cleanup! end - def initialize(app) + def initialize(app, condition=nil) @app = app - end - - module CleanupOnClose - def close - super if defined?(super) - ensure - ActionDispatch::Reloader.cleanup! - end + @condition = condition || lambda { true } + @validated = true end def call(env) - run_callbacks :prepare + @validated = @condition.call + prepare! response = @app.call(env) - response[2].extend(CleanupOnClose) + response[2].extend(module_hook) response rescue Exception - run_callbacks :cleanup + cleanup! raise end + + def prepare! #:nodoc: + run_callbacks :prepare if validated? + end + + def cleanup! #:nodoc: + run_callbacks :cleanup if validated? + ensure + @validated = true + end + + private + + def validated? #:nodoc: + @validated + end + + def module_hook #:nodoc: + middleware = self + Module.new do + define_method :close do + begin + super() if defined?(super) + ensure + middleware.cleanup! + end + end + end + end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 6bcf099d2c..f75b4d4d04 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -74,10 +74,6 @@ module ActionDispatch class AbstractStore < Rack::Session::Abstract::ID include Compatibility include StaleSessionCheck - - def destroy_session(env, sid, options) - raise '#destroy_session needs to be implemented.' - end end end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 8dc2820d37..5eceeece1f 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -1,176 +1,87 @@ -require 'active_support/core_ext/exception' -require 'action_controller/metal/exceptions' -require 'active_support/notifications' require 'action_dispatch/http/request' +require 'action_dispatch/middleware/exception_wrapper' require 'active_support/deprecation' module ActionDispatch - # This middleware rescues any exception returned by the application and renders - # nice exception pages if it's being rescued locally. + # This middleware rescues any exception returned by the application + # and calls an exceptions app that will wrap it in a format for the end user. + # + # The exceptions app should be passed as parameter on initialization + # of ShowExceptions. Everytime there is an exception, ShowExceptions will + # store the exception in env["action_dispatch.exception"], rewrite the + # PATH_INFO to the exception status code and call the rack app. + # + # If the application returns a "X-Cascade" pass response, this middleware + # will send an empty response as result with the correct status code. + # If any exception happens inside the exceptions app, this middleware + # catches the exceptions and returns a FAILSAFE_RESPONSE. class ShowExceptions - RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') - - cattr_accessor :rescue_responses - @@rescue_responses = Hash.new(:internal_server_error) - @@rescue_responses.update({ - 'ActionController::RoutingError' => :not_found, - 'AbstractController::ActionNotFound' => :not_found, - 'ActiveRecord::RecordNotFound' => :not_found, - 'ActiveRecord::StaleObjectError' => :conflict, - 'ActiveRecord::RecordInvalid' => :unprocessable_entity, - 'ActiveRecord::RecordNotSaved' => :unprocessable_entity, - 'ActionController::MethodNotAllowed' => :method_not_allowed, - 'ActionController::NotImplemented' => :not_implemented, - 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity - }) - - cattr_accessor :rescue_templates - @@rescue_templates = Hash.new('diagnostics') - @@rescue_templates.update({ - 'ActionView::MissingTemplate' => 'missing_template', - 'ActionController::RoutingError' => 'routing_error', - 'AbstractController::ActionNotFound' => 'unknown_action', - 'ActionView::Template::Error' => 'template_error' - }) - FAILSAFE_RESPONSE = [500, {'Content-Type' => 'text/html'}, ["<html><body><h1>500 Internal Server Error</h1>" << "If you are the administrator of this website, then please read this web " << "application's log file and/or the web server's log file to find out what " << "went wrong.</body></html>"]] - def initialize(app, consider_all_requests_local = nil) - ActiveSupport::Deprecation.warn "Passing consider_all_requests_local option to ActionDispatch::ShowExceptions middleware no longer works" unless consider_all_requests_local.nil? - @app = app - end - - def call(env) - begin - status, headers, body = @app.call(env) - exception = nil - - # Only this middleware cares about RoutingError. So, let's just raise - # it here. - if headers['X-Cascade'] == 'pass' - raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}" - end - rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - end - - exception ? render_exception(env, exception) : [status, headers, body] - end - - private - def render_exception(env, exception) - log_error(env, exception) - exception = original_exception(exception) - - if env['action_dispatch.show_detailed_exceptions'] == true - rescue_action_diagnostics(env, exception) - else - rescue_action_error_page(exception) - end - rescue Exception => failsafe_error - $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}" - FAILSAFE_RESPONSE - end - - # Render detailed diagnostics for unhandled exceptions rescued from - # a controller action. - def rescue_action_diagnostics(env, exception) - template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], - :request => Request.new(env), - :exception => exception, - :application_trace => application_trace(exception), - :framework_trace => framework_trace(exception), - :full_trace => full_trace(exception) - ) - file = "rescues/#{@@rescue_templates[exception.class.name]}" - body = template.render(:template => file, :layout => 'rescues/layout') - render(status_code(exception), body) - end - - # Attempts to render a static error page based on the - # <tt>status_code</tt> thrown, or just return headers if no such file - # exists. At first, it will try to render a localized static page. - # For example, if a 500 error is being handled Rails and locale is :da, - # it will first attempt to render the file at <tt>public/500.da.html</tt> - # then attempt to render <tt>public/500.html</tt>. If none of them exist, - # the body of the response will be left empty. - def rescue_action_error_page(exception) - status = status_code(exception) - locale_path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale - path = "#{public_path}/#{status}.html" - - if locale_path && File.exist?(locale_path) - render(status, File.read(locale_path)) - elsif File.exist?(path) - render(status, File.read(path)) - else - render(status, '') - end - end - - def status_code(exception) - Rack::Utils.status_code(@@rescue_responses[exception.class.name]) + class << self + def rescue_responses + ActiveSupport::Deprecation.warn "ActionDispatch::ShowExceptions.rescue_responses is deprecated. " \ + "Please configure your exceptions using a railtie or in your application config instead." + ExceptionWrapper.rescue_responses end - def render(status, body) - [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] + def rescue_templates + ActiveSupport::Deprecation.warn "ActionDispatch::ShowExceptions.rescue_templates is deprecated. " \ + "Please configure your exceptions using a railtie or in your application config instead." + ExceptionWrapper.rescue_templates end + end - def public_path - defined?(Rails.public_path) ? Rails.public_path : 'public_path' - end - - def log_error(env, exception) - return unless logger(env) - - ActiveSupport::Deprecation.silence do - message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) - message << " " << application_trace(exception).join("\n ") - logger(env).fatal("#{message}\n\n") - end + def initialize(app, exceptions_app = nil) + if [true, false].include?(exceptions_app) + ActiveSupport::Deprecation.warn "Passing consider_all_requests_local option to ActionDispatch::ShowExceptions middleware no longer works" + exceptions_app = nil end - def application_trace(exception) - clean_backtrace(exception, :silent) + if exceptions_app.nil? + raise ArgumentError, "You need to pass an exceptions_app when initializing ActionDispatch::ShowExceptions. " \ + "In case you want to render pages from a public path, you can use ActionDispatch::PublicExceptions.new('path/to/public')" end - def framework_trace(exception) - clean_backtrace(exception, :noise) - end + @app = app + @exceptions_app = exceptions_app + end - def full_trace(exception) - clean_backtrace(exception, :all) + def call(env) + begin + response = @app.call(env) + rescue Exception => exception + raise exception if env['action_dispatch.show_exceptions'] == false end - def clean_backtrace(exception, *args) - defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ? - Rails.backtrace_cleaner.clean(exception.backtrace, *args) : - exception.backtrace - end + response || render_exception(env, exception) + end - def logger(env) - env['action_dispatch.logger'] || stderr_logger - end + private - def stderr_logger - Logger.new($stderr) - end + # Define this method because some plugins were monkey patching it. + # Remove this after 3.2 is out with the other deprecations in this class. + def status_code(*) + end - def original_exception(exception) - if registered_original_exception?(exception) - exception.original_exception - else - exception - end + def render_exception(env, exception) + wrapper = ExceptionWrapper.new(env, exception) + status = wrapper.status_code + env["action_dispatch.exception"] = wrapper.exception + env["PATH_INFO"] = "/#{status}" + response = @exceptions_app.call(env) + response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response + rescue Exception => failsafe_error + $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}" + FAILSAFE_RESPONSE end - def registered_original_exception?(exception) - exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name) + def pass_response(status) + [status, {"Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0"}, []] end end end diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index f18ebabf29..a4f4825f92 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -9,10 +9,22 @@ module ActionDispatch config.action_dispatch.best_standards_support = true config.action_dispatch.tld_length = 1 config.action_dispatch.ignore_accept_header = false - config.action_dispatch.rack_cache = {:metastore => "rails:/", :entitystore => "rails:/", :verbose => true} + config.action_dispatch.rescue_templates = { } + config.action_dispatch.rescue_responses = { } + + config.action_dispatch.rack_cache = { + :metastore => "rails:/", + :entitystore => "rails:/", + :verbose => true + } + initializer "action_dispatch.configure" do |app| ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header + ActionDispatch::Response.default_charset = app.config.encoding + + ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses) + ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates) config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil? ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 1dcd83ceb5..fa300b4a16 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -277,7 +277,6 @@ module ActionDispatch # module Routing autoload :Mapper, 'action_dispatch/routing/mapper' - autoload :Route, 'action_dispatch/routing/route' autoload :RouteSet, 'action_dispatch/routing/route_set' autoload :RoutesProxy, 'action_dispatch/routing/routes_proxy' autoload :UrlFor, 'action_dispatch/routing/url_for' diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 7947e9d393..4d83c6dee1 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1046,8 +1046,8 @@ module ActionDispatch # Takes same options as <tt>Base#match</tt> as well as: # # [:path_names] - # Allows you to change the paths of the seven default actions. - # Paths not specified are not changed. + # Allows you to change the segment component of the +edit+ and +new+ actions. + # Actions not specified are not changed. # # resources :posts, :path_names => { :new => "brand_new" } # @@ -1286,7 +1286,7 @@ module ActionDispatch action = nil end - if !options.fetch(:as) { true } + if !options.fetch(:as, true) options.delete(:as) else options[:as] = name_for_action(options[:as], action) @@ -1472,8 +1472,16 @@ module ActionDispatch [name_prefix, member_name, prefix] end - candidate = name.select(&:present?).join("_").presence - candidate unless as.nil? && @set.routes.find { |r| r.name == candidate } + if candidate = name.select(&:present?).join("_").presence + # If a name was not explicitly given, we check if it is valid + # and return nil in case it isn't. Otherwise, we pass the invalid name + # forward so the underlying router engine treats it and raises an exception. + if as.nil? + candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i + else + candidate + end + end end end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index e989a38d8b..013cf93dbc 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -165,7 +165,7 @@ module ActionDispatch if parent.is_a?(Symbol) || parent.is_a?(String) parent else - ActiveModel::Naming.route_key(parent).singularize + ActiveModel::Naming.singular_route_key(parent) end end else @@ -176,9 +176,11 @@ module ActionDispatch if record.is_a?(Symbol) || record.is_a?(String) route << record elsif record - route << ActiveModel::Naming.route_key(record) - route = [route.join("_").singularize] if inflection == :singular - route << "index" if ActiveModel::Naming.uncountable?(record) && inflection == :plural + if inflection == :singular + route << ActiveModel::Naming.singular_route_key(record) + else + route << ActiveModel::Naming.route_key(record) + end else raise ArgumentError, "Nil location provided. Can't build URI." end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 2bcde16110..6d6de36a08 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -356,7 +356,7 @@ module ActionDispatch conditions = build_conditions(conditions, valid_conditions, path.names.map { |x| x.to_sym }) route = @set.add_route(app, path, conditions, defaults, name) - named_routes[name] = route if name + named_routes[name] = route if name && !named_routes[name] route end @@ -557,7 +557,7 @@ module ActionDispatch path_addition, params = generate(path_options, path_segments || {}) path << path_addition - ActionDispatch::Http::URL.url_for(options.merge({ + ActionDispatch::Http::URL.url_for(options.merge!({ :path => path, :params => params, :user => user, diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 39ba83fb9a..22e41c9c16 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -145,7 +145,7 @@ module ActionDispatch when String options when nil, Hash - _routes.url_for((options || {}).reverse_merge(url_options).symbolize_keys) + _routes.url_for((options || {}).symbolize_keys.reverse_merge!(url_options)) else polymorphic_url(options) end diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index d7229419a9..c24109a547 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -33,6 +33,7 @@ module ActionView autoload :AssetPaths autoload :Base autoload :Context + autoload :CompiledTemplates, "action_view/context" autoload :Helpers autoload :LookupContext autoload :PathSet diff --git a/actionpack/lib/action_view/asset_paths.rb b/actionpack/lib/action_view/asset_paths.rb index 1d16e34df6..aa5db0d7bc 100644 --- a/actionpack/lib/action_view/asset_paths.rb +++ b/actionpack/lib/action_view/asset_paths.rb @@ -103,8 +103,8 @@ module ActionView if host.respond_to?(:call) args = [source] arity = arity_of(host) - if arity > 1 && !has_request? - invalid_asset_host!("Remove the second argument to your asset_host Proc if you do not need the request.") + if (arity > 1 || arity < -2) && !has_request? + invalid_asset_host!("Remove the second argument to your asset_host Proc if you do not need the request, or make it optional.") end args << current_request if (arity > 1 || arity < 0) && has_request? host.call(*args) diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 36c49d9c91..66a5d59857 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -9,8 +9,8 @@ require 'active_support/core_ext/module/deprecation' module ActionView #:nodoc: # = Action View Base # - # Action View templates can be written in several ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb - # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> (or <tt>.rxml</tt>) extension then Jim Weirich's Builder::XmlMarkup library is used. + # Action View templates can be written in several ways. If the template file has a <tt>.erb</tt> extension then it uses a mixture of ERb + # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> extension then Jim Weirich's Builder::XmlMarkup library is used. # # == ERB # @@ -94,10 +94,10 @@ module ActionView #:nodoc: # # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following: # - # xml.div { + # xml.div do # xml.h1(@person.name) # xml.p(@person.bio) - # } + # end # # would produce something like: # diff --git a/actionpack/lib/action_view/data/encoding_conversions.txt b/actionpack/lib/action_view/data/encoding_conversions.txt deleted file mode 100644 index fdfbe28803..0000000000 --- a/actionpack/lib/action_view/data/encoding_conversions.txt +++ /dev/null @@ -1,88 +0,0 @@ -ASCII-8BIT:UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF-8:ASCII-8BIT,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -US-ASCII:ASCII-8BIT,UTF-8,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Big5:ASCII-8BIT,UTF-8,US-ASCII,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Big5-HKSCS:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Big5-UAO:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -CP949:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -EUC-JP:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -EUC-KR:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -GB18030:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -GBK:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-1:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-2:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-3:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-4:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-5:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-6:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-7:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-8:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-9:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-10:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-11:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-13:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-14:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-8859-15:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -KOI8-R:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -KOI8-U:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Shift_JIS:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF-16BE:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF-16LE:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF-32BE:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF-32LE:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Windows-1251:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM437:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM737:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM775:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -CP850:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM852:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -CP852:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM855:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -CP855:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM857:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM860:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM861:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM862:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM863:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM865:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM866:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -IBM869:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -macCroatian:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -macCyrillic:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -macGreek:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -macIceland:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -macRoman:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -macRomania:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -macTurkish:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -macUkraine:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -CP950:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -CP951:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -stateless-ISO-2022-JP:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -eucJP-ms:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -CP51932:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -GB2312:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -GB12345:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-2022-JP:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -CP50220:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -CP50221:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Windows-1252:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Windows-1250:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Windows-1256:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Windows-1253:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Windows-1255:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Windows-1254:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -TIS-620:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Windows-874:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Windows-1257:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -Windows-31J:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF8-MAC:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF-16:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF-32:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF8-DoCoMo:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -SJIS-DoCoMo:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF8-KDDI:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -SJIS-KDDI:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -ISO-2022-JP-KDDI:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -stateless-ISO-2022-JP-KDDI:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,UTF8-SoftBank,SJIS-SoftBank -UTF8-SoftBank:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,SJIS-SoftBank -SJIS-SoftBank:ASCII-8BIT,UTF-8,US-ASCII,Big5,Big5-HKSCS,Big5-UAO,CP949,EUC-JP,EUC-KR,GB18030,GBK,ISO-8859-1,ISO-8859-2,ISO-8859-3,ISO-8859-4,ISO-8859-5,ISO-8859-6,ISO-8859-7,ISO-8859-8,ISO-8859-9,ISO-8859-10,ISO-8859-11,ISO-8859-13,ISO-8859-14,ISO-8859-15,KOI8-R,KOI8-U,Shift_JIS,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,Windows-1251,IBM437,IBM737,IBM775,CP850,IBM852,CP852,IBM855,CP855,IBM857,IBM860,IBM861,IBM862,IBM863,IBM865,IBM866,IBM869,macCroatian,macCyrillic,macGreek,macIceland,macRoman,macRomania,macTurkish,macUkraine,CP950,CP951,stateless-ISO-2022-JP,eucJP-ms,CP51932,GB2312,GB12345,ISO-2022-JP,CP50220,CP50221,Windows-1252,Windows-1250,Windows-1256,Windows-1253,Windows-1255,Windows-1254,TIS-620,Windows-874,Windows-1257,Windows-31J,UTF8-MAC,UTF-16,UTF-32,UTF8-DoCoMo,SJIS-DoCoMo,UTF8-KDDI,SJIS-KDDI,ISO-2022-JP-KDDI,stateless-ISO-2022-JP-KDDI,UTF8-SoftBank
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb index 90589d2238..73824dc1f8 100644 --- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -32,7 +32,7 @@ module ActionView # app/views/posts/index.atom.builder: # atom_feed do |feed| # feed.title("My great blog!") - # feed.updated(@posts.first.created_at) if @posts.any? + # feed.updated(@posts[0].created_at) if @posts.length > 0 # # @posts.each do |post| # feed.entry(post) do |entry| diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 4deb87180c..dd2b59cb0a 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -736,7 +736,7 @@ module ActionView if @options[:use_hidden] || @options[:discard_day] build_hidden(:day, day) else - build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false) + build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false, :use_two_digit_numbers => @options[:use_two_digit_numbers]) end end @@ -822,6 +822,8 @@ module ActionView def month_name(number) if @options[:use_month_numbers] number + elsif @options[:use_two_digit_numbers] + sprintf "%02d", number elsif @options[:add_month_numbers] "#{number} - #{month_names[number]}" else @@ -857,7 +859,7 @@ module ActionView start = options.delete(:start) || 0 stop = options.delete(:end) || 59 step = options.delete(:step) || 1 - options.reverse_merge!({:leading_zeros => true, :ampm => false}) + options.reverse_merge!({:leading_zeros => true, :ampm => false, :use_two_digit_numbers => false}) leading_zeros = options.delete(:leading_zeros) select_options = [] @@ -865,7 +867,8 @@ module ActionView value = leading_zeros ? sprintf("%02d", i) : i tag_options = { :value => value } tag_options[:selected] = "selected" if selected == i - text = options[:ampm] ? AMPM_TRANSLATION[i] : value + text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value + text = options[:ampm] ? AMPM_TRANSLATION[i] : text select_options << content_tag(:option, text, tag_options) end (select_options.join("\n") + "\n").html_safe @@ -944,8 +947,9 @@ module ActionView # and join them with their appropriate separators. def build_selects_from_types(order) select = '' + first_visible = order.find { |type| !@options[:"discard_#{type}"] } order.reverse.each do |type| - separator = separator(type) unless type == order.first # don't add on last field + separator = separator(type) unless type == first_visible # don't add before first visible field select.insert(0, separator.to_s + send("select_#{type}").to_s) end select.html_safe diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 6c97d75922..ccb2275329 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -160,7 +160,7 @@ module ActionView # here a named route directly as well. Defaults to the current action. # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of # id attributes on form elements. The namespace attribute will be prefixed - # with underscore on the generate HTML id. + # with underscore on the generated HTML id. # * <tt>:html</tt> - Optional HTML attributes for the form tag. # # Also note that +form_for+ doesn't create an exclusive scope. It's still @@ -387,8 +387,8 @@ module ActionView as = options[:as] action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :put] : [:new, :post] options[:html].reverse_merge!( - :class => as ? "#{as}_#{action}" : dom_class(object, action), - :id => as ? "#{as}_#{action}" : [options[:namespace], dom_id(object, action)].compact.join("_").presence, + :class => as ? "#{action}_#{as}" : dom_class(object, action), + :id => as ? "#{action}_#{as}" : [options[:namespace], dom_id(object, action)].compact.join("_").presence, :method => method ) @@ -1364,6 +1364,39 @@ module ActionView @template.submit_tag(value, 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: + # + # <%= form_for @post do |f| %> + # <%= f.button %> + # <% end %> + # + # In the example above, if @post is a new record, it will use "Create Post" as + # submit button label, otherwise, it uses "Update Post". + # + # Those labels can be customized using I18n, under the helpers.submit key and accept + # the %{model} as translation interpolation: + # + # en: + # helpers: + # button: + # create: "Create a %{model}" + # update: "Confirm changes to %{model}" + # + # It also searches for a key specific for the given object: + # + # en: + # helpers: + # button: + # post: + # create: "Add %{model}" + # + def button(value=nil, options={}) + value, options = nil, value if value.is_a?(Hash) + value ||= submit_default_value + @template.button_tag(value, options) + end + def emitted_hidden_id? @emitted_hidden_id ||= nil end diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb index bcc8f6fbcb..7768c8c151 100644 --- a/actionpack/lib/action_view/helpers/sanitize_helper.rb +++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/object/try' require 'action_controller/vendor/html-scanner' -require 'action_view/helpers/tag_helper' module ActionView # = Action View Sanitize Helpers diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index bc8572fe69..209360ee82 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/filters' -require 'action_view/helpers/tag_helper' module ActionView # = Action View Text Helpers @@ -31,6 +30,7 @@ module ActionView extend ActiveSupport::Concern include SanitizeHelper + include TagHelper # The preferred method of outputting text in your views is to use the # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods # do not operate as expected in an eRuby code block. If you absolutely must diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index eb816b9446..f2a83b92a9 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -152,3 +152,9 @@ update: 'Update %{model}' submit: 'Save %{model}' + # Default translation keys for button FormHelper + button: + create: 'Create %{model}' + update: 'Update %{model}' + submit: 'Save %{model}' + diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index fa4bf70f77..3bb2b98e48 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -20,7 +20,7 @@ module ActionView def self.register_detail(name, options = {}, &block) self.registered_details << name - initialize = registered_details.map { |n| "self.#{n} = details[:#{n}]" } + initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" } Accessors.send :define_method, :"default_#{name}", &block Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 @@ -29,7 +29,7 @@ module ActionView end def #{name}=(value) - value = Array.wrap(value.presence || default_#{name}) + value = value.present? ? Array.wrap(value) : default_#{name} _set_detail(:#{name}, value) if value != @details[:#{name}] end @@ -44,7 +44,7 @@ module ActionView module Accessors #:nodoc: end - register_detail(:locale) { [I18n.locale, I18n.default_locale] } + register_detail(:locale) { [I18n.locale, I18n.default_locale].uniq } register_detail(:formats) { Mime::SET.symbols } register_detail(:handlers){ Template::Handlers.extensions } @@ -56,7 +56,11 @@ module ActionView @details_keys = Hash.new def self.get(details) - @details_keys[details.freeze] ||= new + @details_keys[details] ||= new + end + + def self.clear + @details_keys.clear end def initialize @@ -85,9 +89,9 @@ module ActionView protected def _set_detail(key, value) + @details = @details.dup if @details_key @details_key = nil - @details = @details.dup if @details.frozen? - @details[key] = value.freeze + @details[key] = value end end @@ -153,14 +157,14 @@ module ActionView "" end - parts = name.split('/') - name = parts.pop + prefixes = nil if prefixes.blank? + parts = name.split('/') + name = parts.pop - prefixes = if prefixes.blank? - [parts.join('/')] - else - prefixes.map { |prefix| [prefix, *parts].compact.join('/') } - end + return name, prefixes || [""] if parts.empty? + + parts = parts.join('/') + prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts] return name, prefixes end @@ -227,7 +231,6 @@ module ActionView end # A method which only uses the first format in the formats array for layout lookup. - # This method plays straight with instance variables for performance reasons. def with_layout_format if formats.size == 1 yield diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 54a0ba96ff..374bdb62f5 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -82,16 +82,18 @@ module ActionView # # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from. # - # == Rendering objects with the RecordIdentifier + # == Rendering objects that respond to `to_partial_path` # - # Instead of explicitly naming the location of a partial, you can also let the RecordIdentifier do the work if - # you're following its conventions for RecordIdentifier#partial_path. Examples: + # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work + # and pick the proper path by checking `to_proper_path` method. If the object passed to render is a collection, + # all objects must return the same path. # - # # @account is an Account instance, so it uses the RecordIdentifier to replace + # # @account.to_partial_path returns 'accounts/account', so it can be used to replace: # # <%= render :partial => "accounts/account", :locals => { :account => @account} %> # <%= render :partial => @account %> # - # # @posts is an array of Post instances, so it uses the RecordIdentifier to replace + # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`, + # # that's why we can replace: # # <%= render :partial => "posts/post", :collection => @posts %> # <%= render :partial => @posts %> # @@ -106,13 +108,14 @@ module ActionView # # Instead of <%= render :partial => "account", :locals => { :account => @buyer } %> # <%= render "account", :account => @buyer %> # - # # @account is an Account instance, so it uses the RecordIdentifier to replace - # # <%= render :partial => "accounts/account", :locals => { :account => @account } %> - # <%= render(@account) %> + # # @account.to_partial_path returns 'accounts/account', so it can be used to replace: + # # <%= render :partial => "accounts/account", :locals => { :account => @account} %> + # <%= render @account %> # - # # @posts is an array of Post instances, so it uses the RecordIdentifier to replace + # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`, + # # that's why we can replace: # # <%= render :partial => "posts/post", :collection => @posts %> - # <%= render(@posts) %> + # <%= render @posts %> # # == Rendering partials with layouts # @@ -205,7 +208,7 @@ module ActionView # Deadline: <%= user.deadline %> # <%- end -%> # <% end %> - class PartialRenderer < AbstractRenderer #:nodoc: + class PartialRenderer < AbstractRenderer PARTIAL_NAMES = Hash.new { |h,k| h[k] = {} } def initialize(*) @@ -375,23 +378,23 @@ module ActionView end end - @partial_names[path] ||= path.dup.tap do |object_path| - merge_prefix_into_object_path(@context_prefix, object_path) - end + @partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup) end def merge_prefix_into_object_path(prefix, object_path) if prefix.include?(?/) && object_path.include?(?/) - overlap = [] + prefixes = [] prefix_array = File.dirname(prefix).split('/') object_path_array = object_path.split('/')[0..-3] # skip model dir & partial prefix_array.each_with_index do |dir, index| - overlap << dir if dir == object_path_array[index] + break if dir == object_path_array[index] + prefixes << dir end - object_path.gsub!(/^#{overlap.join('/')}\//,'') - object_path.insert(0, "#{File.dirname(prefix)}/") + (prefixes << object_path).join("/") + else + object_path end end diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb index ac91d333ba..e131afa279 100644 --- a/actionpack/lib/action_view/renderer/template_renderer.rb +++ b/actionpack/lib/action_view/renderer/template_renderer.rb @@ -58,14 +58,28 @@ module ActionView # context object. If no layout is found, it checks if at least a layout with # the given name exists across all details before raising the error. def find_layout(layout, keys) - begin - with_layout_format do - layout =~ /^\// ? - with_fallbacks { find_template(layout, nil, false, keys, @details) } : find_template(layout, nil, false, keys, @details) + with_layout_format { resolve_layout(layout, keys) } + end + + def resolve_layout(layout, keys) + case layout + when String + begin + if layout =~ /^\// + with_fallbacks { find_template(layout, nil, false, keys, @details) } + else + find_template(layout, nil, false, keys, @details) + end + rescue ActionView::MissingTemplate + all_details = @details.merge(:formats => @lookup_context.default_formats) + raise unless template_exists?(layout, nil, false, keys, all_details) end - rescue ActionView::MissingTemplate - all_details = @details.merge(:formats => @lookup_context.default_formats) - raise unless template_exists?(layout, nil, false, keys, all_details) + when Proc + resolve_layout(layout.call, keys) + when FalseClass + nil + else + layout end end end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 8d69880308..10797c010f 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -3,33 +3,6 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' require 'active_support/core_ext/kernel/singleton_class' -if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' && RUBY_VERSION == '1.9.3' && RUBY_PATCHLEVEL == 0 - # This is a hack to work around a bug in Ruby 1.9.3p0: - # http://redmine.ruby-lang.org/issues/5564 - # - # Basically, at runtime we may need to perform some encoding conversions on the templates, - # but if the converter hasn't been loaded by Ruby beforehand (i.e. now), then it won't be - # able to find it (due to a bug). - # - # However, we don't know what conversions we may need to do a runtime. So we load up a - # text file which contains a pre-generated list of all the possible conversions, - # and we load all of them. - # - # In my testing this increased the process size by about 3.9 MB (after the conversions array - # is GC'd) and took around 170ms to run, which seems acceptable for a workaround. - # - # The script to dump the conversions is: https://gist.github.com/1371499 - - filename = File.join(File.dirname(__FILE__), 'data', 'encoding_conversions.txt') - conversions = File.read(filename) - conversions.split("\n").map do |line| - from, to_array = line.split(':') - to_array.split(',').each do |to| - Encoding::Converter.new(from, to) - end - end -end - module ActionView # = Action View Template class Template diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb index 587e37a84f..fe27e54037 100644 --- a/actionpack/lib/action_view/template/error.rb +++ b/actionpack/lib/action_view/template/error.rb @@ -89,10 +89,14 @@ module ActionView line_counter = start_on_line return unless source_code = source_code[start_on_line..end_on_line] - source_code.sum do |line| + extract = source_code.sum do |line| line_counter += 1 "#{indent}#{line_counter}: #{line}\n" end + + extract.encode! if extract.respond_to?(:encode!) + + extract end def sub_template_of(template_path) diff --git a/actionpack/lib/action_view/template/handlers/builder.rb b/actionpack/lib/action_view/template/handlers/builder.rb index 2c52cfd90e..34397c3bcf 100644 --- a/actionpack/lib/action_view/template/handlers/builder.rb +++ b/actionpack/lib/action_view/template/handlers/builder.rb @@ -6,12 +6,21 @@ module ActionView self.default_format = Mime::XML def call(template) - require 'builder' + require_engine "xml = ::Builder::XmlMarkup.new(:indent => 2);" + "self.output_buffer = xml.target!;" + template.source + ";xml.target!;" end + + protected + + def require_engine + @required ||= begin + require "builder" + true + end + end end end end diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index 77720e2bc8..25f26dd609 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -1,5 +1,5 @@ require 'action_dispatch/http/mime_type' -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/class/attribute' require 'erubis' module ActionView diff --git a/actionpack/lib/sprockets/helpers/rails_helper.rb b/actionpack/lib/sprockets/helpers/rails_helper.rb index 1ebe7f68f7..1bd9d5ca26 100644 --- a/actionpack/lib/sprockets/helpers/rails_helper.rb +++ b/actionpack/lib/sprockets/helpers/rails_helper.rb @@ -26,10 +26,10 @@ module Sprockets sources.collect do |source| if debug && asset = asset_paths.asset_for(source, 'js') asset.to_a.map { |dep| - super(dep.to_s, { :src => asset_path(dep, :ext => 'js', :body => true, :digest => digest) }.merge!(options)) + super(dep.pathname.to_s, { :src => path_to_asset(dep, :ext => 'js', :body => true, :digest => digest) }.merge!(options)) } else - super(source.to_s, { :src => asset_path(source, :ext => 'js', :body => body, :digest => digest) }.merge!(options)) + super(source.to_s, { :src => path_to_asset(source, :ext => 'js', :body => body, :digest => digest) }.merge!(options)) end end.join("\n").html_safe end @@ -43,10 +43,10 @@ module Sprockets sources.collect do |source| if debug && asset = asset_paths.asset_for(source, 'css') asset.to_a.map { |dep| - super(dep.to_s, { :href => asset_path(dep, :ext => 'css', :body => true, :protocol => :request, :digest => digest) }.merge!(options)) + super(dep.pathname.to_s, { :href => path_to_asset(dep, :ext => 'css', :body => true, :protocol => :request, :digest => digest) }.merge!(options)) } else - super(source.to_s, { :href => asset_path(source, :ext => 'css', :body => body, :protocol => :request, :digest => digest) }.merge!(options)) + super(source.to_s, { :href => path_to_asset(source, :ext => 'css', :body => body, :protocol => :request, :digest => digest) }.merge!(options)) end end.join("\n").html_safe end @@ -56,19 +56,20 @@ module Sprockets path = asset_paths.compute_public_path(source, asset_prefix, options.merge(:body => true)) options[:body] ? "#{path}?body=1" : path end + alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with an asset_path named route def image_path(source) - asset_path(source) + path_to_asset(source) end alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route def javascript_path(source) - asset_path(source) + path_to_asset(source) end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with an javascript_path named route def stylesheet_path(source) - asset_path(source) + path_to_asset(source) end alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with an stylesheet_path named route diff --git a/actionpack/lib/sprockets/railtie.rb b/actionpack/lib/sprockets/railtie.rb index 3d330bd91a..a7eb03acaf 100644 --- a/actionpack/lib/sprockets/railtie.rb +++ b/actionpack/lib/sprockets/railtie.rb @@ -10,8 +10,6 @@ module Sprockets # TODO: Get rid of config.assets.enabled class Railtie < ::Rails::Railtie - config.action_controller.default_asset_host_protocol = :relative - rake_tasks do load "sprockets/assets.rake" end diff --git a/actionpack/test/abstract/layouts_test.rb b/actionpack/test/abstract/layouts_test.rb index 86208899f8..de6f42d826 100644 --- a/actionpack/test/abstract/layouts_test.rb +++ b/actionpack/test/abstract/layouts_test.rb @@ -141,6 +141,30 @@ module AbstractControllerTests end end + class WithOnlyConditional < WithStringImpliedChild + layout "overwrite", :only => :show + + def index + render :template => ActionView::Template::Text.new("Hello index!") + end + + def show + render :template => ActionView::Template::Text.new("Hello show!") + end + end + + class WithExceptConditional < WithStringImpliedChild + layout "overwrite", :except => :show + + def index + render :template => ActionView::Template::Text.new("Hello index!") + end + + def show + render :template => ActionView::Template::Text.new("Hello show!") + end + end + class TestBase < ActiveSupport::TestCase test "when no layout is specified, and no default is available, render without a layout" do controller = Blank.new @@ -234,7 +258,7 @@ module AbstractControllerTests test "when a child controller has an implied layout, use that layout and not the parent controller layout" do controller = WithStringImpliedChild.new - controller.process(:index) + assert_deprecated { controller.process(:index) } assert_equal "With Implied Hello string!", controller.response_body end @@ -247,7 +271,7 @@ module AbstractControllerTests test "when a grandchild has no layout specified, the child has an implied layout, and the " \ "parent has specified a layout, use the child controller layout" do controller = WithChildOfImplied.new - controller.process(:index) + assert_deprecated { controller.process(:index) } assert_equal "With Implied Hello string!", controller.response_body end @@ -260,6 +284,30 @@ module AbstractControllerTests end end end + + test "when specify an :only option which match current action name" do + controller = WithOnlyConditional.new + controller.process(:show) + assert_equal "Overwrite Hello show!", controller.response_body + end + + test "when specify an :only option which does not match current action name" do + controller = WithOnlyConditional.new + controller.process(:index) + assert_equal "With Implied Hello index!", controller.response_body + end + + test "when specify an :except option which match current action name" do + controller = WithExceptConditional.new + controller.process(:show) + assert_equal "With Implied Hello show!", controller.response_body + end + + test "when specify an :except option which does not match current action name" do + controller = WithExceptConditional.new + controller.process(:index) + assert_equal "Overwrite Hello index!", controller.response_body + end end end -end
\ No newline at end of file +end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index cbb8968496..40ca23a39d 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -73,7 +73,17 @@ module RackTestUtils end module RenderERBUtils + def view + @view ||= begin + path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH) + view_paths = ActionView::PathSet.new([path]) + ActionView::Base.new(view_paths) + end + end + def render_erb(string) + @virtual_path = nil + template = ActionView::Template.new( string.strip, "test template", @@ -164,7 +174,8 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase def self.build_app(routes = nil) RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| - middleware.use "ActionDispatch::ShowExceptions" + middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") + middleware.use "ActionDispatch::DebugExceptions" middleware.use "ActionDispatch::Callbacks" middleware.use "ActionDispatch::ParamsParser" middleware.use "ActionDispatch::Cookies" @@ -326,18 +337,18 @@ class Workshop end module ActionDispatch - class ShowExceptions + class DebugExceptions private - remove_method :public_path - def public_path - "#{FIXTURE_LOAD_PATH}/public" - end - - remove_method :stderr_logger - # Silence logger - def stderr_logger - nil - end + remove_method :stderr_logger + # Silence logger + def stderr_logger + nil + end end end +module RoutingTestHelpers + def url_for(set, options, recall = nil) + set.send(:url_for, options.merge(:only_path => true, :_path_segments => recall)) + end +end diff --git a/actionpack/test/activerecord/polymorphic_routes_test.rb b/actionpack/test/activerecord/polymorphic_routes_test.rb index fc829aa6b4..90e7f4ae59 100644 --- a/actionpack/test/activerecord/polymorphic_routes_test.rb +++ b/actionpack/test/activerecord/polymorphic_routes_test.rb @@ -2,36 +2,36 @@ require 'active_record_unit' require 'fixtures/project' class Task < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' end class Step < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' end class Bid < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' end class Tax < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' end class Fax < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' end class Series < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' end module Blog class Post < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' end class Blog < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' end def self.use_relative_model_naming? diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index a714e8bbcc..b414327d08 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -71,6 +71,10 @@ class ActionPackAssertionsController < ActionController::Base render :text => "Hello!", :content_type => Mime::RSS end + def render_with_layout + render "test/hello_world", :layout => "layouts/standard" + end + def session_stuffing session['xmas'] = 'turkey' render :text => "ho ho ho" @@ -471,6 +475,18 @@ class AssertTemplateTest < ActionController::TestCase end end + def test_fails_with_wrong_layout + get :render_with_layout + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_template :layout => "application" + end + end + + def test_passes_with_correct_layout + get :render_with_layout + assert_template :layout => "layouts/standard" + end + def test_assert_template_reset_between_requests get :hello_world assert_template 'test/hello_world' diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 2364bbf3a3..015e6b9955 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -251,6 +251,11 @@ class ActionCachingTestController < CachingController expire_action :controller => 'action_caching_test', :action => 'index', :format => 'xml' render :nothing => true end + + def expire_with_url_string + expire_action url_for(:controller => 'action_caching_test', :action => 'index') + render :nothing => true + end end class MockTime < Time @@ -445,6 +450,21 @@ class ActionCacheTest < ActionController::TestCase assert_not_equal cached_time, @response.body end + def test_cache_expiration_with_url_string + get :index + cached_time = content_to_cache + reset! + + @request.request_uri = "/action_caching_test/expire_with_url_string" + get :expire_with_url_string + assert_response :success + reset! + + get :index + assert_response :success + assert_not_equal cached_time, @response.body + end + def test_cache_is_scoped_by_subdomain @request.host = 'jamis.hostname.com' get :index diff --git a/actionpack/test/controller/default_url_options_with_filter_test.rb b/actionpack/test/controller/default_url_options_with_filter_test.rb index 3bbb981040..ef028e8cdb 100644 --- a/actionpack/test/controller/default_url_options_with_filter_test.rb +++ b/actionpack/test/controller/default_url_options_with_filter_test.rb @@ -21,7 +21,7 @@ end class ControllerWithBeforeFilterAndDefaultUrlOptionsTest < ActionController::TestCase - # This test has it´s roots in issue #1872 + # This test has its roots in issue #1872 test "should redirect with correct locale :de" do get :redirect, :locale => "de" assert_redirected_to "/controller_with_before_filter_and_default_url_options/target?locale=de" diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index e19612eace..d497913dc4 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -254,16 +254,6 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest end end - def test_setting_flash_raises_after_stream_back_to_client - with_test_route_set do - env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } - get '/set_flash', nil, env - assert_raise(ActionDispatch::ClosedError) { - @request.flash['alert'] = 'alert' - } - end - end - def test_setting_flash_does_not_raise_in_following_requests with_test_route_set do env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } @@ -280,36 +270,6 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest end end - def test_setting_flash_raises_after_stream_back_to_client_even_with_an_empty_flash - with_test_route_set do - env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } - get '/dont_set_flash', nil, env - assert_raise(ActionDispatch::ClosedError) { - @request.flash['alert'] = 'alert' - } - end - end - - def test_setting_flash_now_raises_after_stream_back_to_client - with_test_route_set do - env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } - get '/set_flash_now', nil, env - assert_raise(ActionDispatch::ClosedError) { - @request.flash.now['alert'] = 'alert' - } - end - end - - def test_setting_flash_now_raises_after_stream_back_to_client_even_with_an_empty_flash - with_test_route_set do - env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } - get '/dont_set_flash', nil, env - assert_raise(ActionDispatch::ClosedError) { - @request.flash.now['alert'] = 'alert' - } - end - end - private # Overwrite get to send SessionSecret in env hash diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index 25299eb8b8..bc171e201b 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -167,7 +167,7 @@ class LayoutSetInResponseTest < ActionController::TestCase def test_layout_is_picked_from_the_controller_instances_view_path @controller = PrependsViewPathController.new get :hello - assert_template :layout => /layouts\/alt\.\w+/ + assert_template :layout => /layouts\/alt/ end def test_absolute_pathed_layout diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index ccdfcb0b2c..700fd788fa 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -13,6 +13,11 @@ module Another head :status => 406 end + before_filter :redirector, :only => :never_executed + + def never_executed + end + def show render :nothing => true end @@ -49,7 +54,6 @@ module Another def with_rescued_exception raise SpecialException end - end end @@ -86,6 +90,13 @@ class ACLogSubscriberTest < ActionController::TestCase assert_equal "Processing by Another::LogSubscribersController#show as HTML", logs.first end + def test_halted_callback + get :never_executed + wait + assert_equal 4, logs.size + assert_equal "Filter chain halted as :redirector rendered or redirected", logs.third + end + def test_process_action get :show wait diff --git a/actionpack/test/controller/new_base/render_layout_test.rb b/actionpack/test/controller/new_base/render_layout_test.rb index d3dcb5cad6..4ac40ca405 100644 --- a/actionpack/test/controller/new_base/render_layout_test.rb +++ b/actionpack/test/controller/new_base/render_layout_test.rb @@ -71,7 +71,8 @@ module ControllerLayouts self.view_paths = [ActionView::FixtureResolver.new( "layouts/application.html.erb" => "<html><%= yield %></html>", "controller_layouts/mismatch_format/index.xml.builder" => "xml.instruct!", - "controller_layouts/mismatch_format/implicit.builder" => "xml.instruct!" + "controller_layouts/mismatch_format/implicit.builder" => "xml.instruct!", + "controller_layouts/mismatch_format/explicit.js.erb" => "alert('foo');" )] def explicit @@ -81,7 +82,7 @@ module ControllerLayouts class MismatchFormatTest < Rack::TestCase testing ControllerLayouts::MismatchFormatController - + XML_INSTRUCT = %Q(<?xml version="1.0" encoding="UTF-8"?>\n) test "if XML is selected, an HTML template is not also selected" do @@ -94,10 +95,33 @@ module ControllerLayouts assert_response XML_INSTRUCT end - test "if an HTML template is explicitly provides for a JS template, an error is raised" do - assert_raises ActionView::MissingTemplate do - get :explicit, {}, "action_dispatch.show_exceptions" => false - end + test "a layout for JS is ignored even if explicitly provided for HTML" do + get :explicit, { :format => "js" } + assert_response "alert('foo');" + end + end + + class FalseLayoutMethodController < ::ApplicationController + self.view_paths = [ActionView::FixtureResolver.new( + "controller_layouts/false_layout_method/index.js.erb" => "alert('foo');" + )] + + layout :which_layout? + + def which_layout? + false + end + + def index + end + end + + class FalseLayoutMethodTest < Rack::TestCase + testing ControllerLayouts::FalseLayoutMethodController + + test "access false layout returned by a method/proc" do + get :index, :format => "js" + assert_response "alert('foo');" 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 ba804421da..ade204c387 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -59,6 +59,12 @@ module RenderTemplate def with_error render :template => "test/with_error" end + + private + + def show_detailed_exceptions? + request.local? + end end class TestWithoutLayout < Rack::TestCase diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index 7bef1e8d5d..c4d2614200 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -147,6 +147,7 @@ class ParamsWrapperTest < ActionController::TestCase end def test_derived_wrapped_keys_from_matching_model + User.expects(:respond_to?).with(:accessible_attributes).returns(false) User.expects(:respond_to?).with(:attribute_names).returns(true) User.expects(:attribute_names).twice.returns(["username"]) @@ -159,6 +160,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_derived_wrapped_keys_from_specified_model with_default_wrapper_options do + Person.expects(:respond_to?).with(:accessible_attributes).returns(false) Person.expects(:respond_to?).with(:attribute_names).returns(true) Person.expects(:attribute_names).twice.returns(["username"]) @@ -169,8 +171,33 @@ class ParamsWrapperTest < ActionController::TestCase assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) end end + + def test_accessible_wrapped_keys_from_matching_model + User.expects(:respond_to?).with(:accessible_attributes).returns(true) + User.expects(:accessible_attributes).twice.returns(["username"]) + + with_default_wrapper_options do + @request.env['CONTENT_TYPE'] = 'application/json' + post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }}) + end + end + + def test_accessible_wrapped_keys_from_specified_model + with_default_wrapper_options do + Person.expects(:respond_to?).with(:accessible_attributes).returns(true) + Person.expects(:accessible_attributes).twice.returns(["username"]) + + UsersController.wrap_parameters Person + + @request.env['CONTENT_TYPE'] = 'application/json' + post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) + end + end def test_not_wrapping_abstract_model + User.expects(:respond_to?).with(:accessible_attributes).returns(false) User.expects(:respond_to?).with(:attribute_names).returns(true) User.expects(:attribute_names).returns([]) @@ -285,3 +312,38 @@ class AnonymousControllerParamsWrapperTest < ActionController::TestCase end end end + +class IrregularInflectionParamsWrapperTest < ActionController::TestCase + include ParamsWrapperTestHelp + + class ParamswrappernewsItem + def self.attribute_names + ['test_attr'] + end + end + + class ParamswrappernewsController < ActionController::Base + class << self + attr_accessor :last_parameters + end + + def parse + self.class.last_parameters = request.params.except(:controller, :action) + head :ok + end + end + + tests ParamswrappernewsController + + def test_uses_model_attribute_names_with_irregular_inflection + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'paramswrappernews_item', 'paramswrappernews' + end + + with_default_wrapper_options do + @request.env['CONTENT_TYPE'] = 'application/json' + post :parse, { 'username' => 'sikachu', 'test_attr' => 'test_value' } + assert_parameters({ 'username' => 'sikachu', 'test_attr' => 'test_value', 'paramswrappernews_item' => { 'test_attr' => 'test_value' }}) + end + end +end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 79041055bd..5b739e49ac 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -70,6 +70,10 @@ class RedirectController < ActionController::Base redirect_to "x-test+scheme.complex:redirect" end + def redirect_to_url_with_network_path_reference + redirect_to "//www.rubyonrails.org/" + end + def redirect_to_back redirect_to :back end @@ -216,6 +220,12 @@ class RedirectTest < ActionController::TestCase assert_equal "x-test+scheme.complex:redirect", redirect_to_url end + def test_redirect_to_url_with_network_path_reference + get :redirect_to_url_with_network_path_reference + assert_response :redirect + assert_equal "//www.rubyonrails.org/", redirect_to_url + end + def test_redirect_to_back @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" get :redirect_to_back diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index aea603b014..243bad8749 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -50,12 +50,28 @@ class TestController < ActionController::Base end end + def conditional_hello_with_record + record = Struct.new(:updated_at, :cache_key).new(Time.now.utc.beginning_of_day, "foo/123") + + if stale?(record) + render :action => 'hello_world' + end + end + def conditional_hello_with_public_header if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true) render :action => 'hello_world' end end + def conditional_hello_with_public_header_with_record + record = Struct.new(:updated_at, :cache_key).new(Time.now.utc.beginning_of_day, "foo/123") + + if stale?(record, :public => true) + render :action => 'hello_world' + end + end + def conditional_hello_with_public_header_and_expires_at expires_in 1.minute if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true) @@ -1440,6 +1456,36 @@ class LastModifiedRenderTest < ActionController::TestCase assert_equal @last_modified, @response.headers['Last-Modified'] end + + def test_responds_with_last_modified_with_record + get :conditional_hello_with_record + assert_equal @last_modified, @response.headers['Last-Modified'] + end + + def test_request_not_modified_with_record + @request.if_modified_since = @last_modified + get :conditional_hello_with_record + assert_equal 304, @response.status.to_i + assert_blank @response.body + assert_equal @last_modified, @response.headers['Last-Modified'] + end + + def test_request_not_modified_but_etag_differs_with_record + @request.if_modified_since = @last_modified + @request.if_none_match = "234" + get :conditional_hello_with_record + assert_response :success + end + + def test_request_modified_with_record + @request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT' + get :conditional_hello_with_record + assert_equal 200, @response.status.to_i + assert_present @response.body + assert_equal @last_modified, @response.headers['Last-Modified'] + end + + def test_request_with_bang_gets_last_modified get :conditional_hello_with_bangs assert_equal @last_modified, @response.headers['Last-Modified'] diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 5bf68decca..062fc1f94e 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -11,12 +11,6 @@ end ROUTING = ActionDispatch::Routing -module RoutingTestHelpers - def url_for(set, options, recall = nil) - set.send(:url_for, options.merge(:only_path => true, :_path_segments => recall)) - end -end - # See RFC 3986, section 3.3 for allowed path characters. class UriReservedCharactersRoutingTest < Test::Unit::TestCase include RoutingTestHelpers @@ -719,12 +713,12 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal set.routes.first, set.named_routes[:hello] end - def test_later_named_routes_take_precedence + def test_earlier_named_routes_take_precedence set.draw do match '/hello/world' => 'a#b', :as => 'hello' match '/hello' => 'a#b', :as => 'hello' end - assert_equal set.routes.last, set.named_routes[:hello] + assert_equal set.routes.first, set.named_routes[:hello] end def setup_named_route_test diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb index 74067cb895..13ab19ed8f 100644 --- a/actionpack/test/controller/show_exceptions_test.rb +++ b/actionpack/test/controller/show_exceptions_test.rb @@ -2,11 +2,24 @@ require 'abstract_unit' module ShowExceptions class ShowExceptionsController < ActionController::Base - use ActionDispatch::ShowExceptions + use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") + use ActionDispatch::DebugExceptions + + before_filter :only => :another_boom do + request.env["action_dispatch.show_detailed_exceptions"] = true + end def boom raise 'boom!' end + + def another_boom + raise 'boom!' + end + + def show_detailed_exceptions? + request.local? + end end class ShowExceptionsTest < ActionDispatch::IntegrationTest @@ -17,7 +30,7 @@ module ShowExceptions assert_equal "500 error fixture\n", body end - test 'show diagnostics from a local ip' do + test 'show diagnostics from a local ip if show_detailed_exceptions? is set to request.local?' do @app = ShowExceptionsController.action(:boom) ['127.0.0.1', '127.0.0.127', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address| self.remote_addr = ip_address @@ -26,9 +39,8 @@ module ShowExceptions end end - test 'show diagnostics from a remote ip when consider_all_requests_local is true' do - ShowExceptionsController.any_instance.stubs(:consider_all_requests_local).returns(true) - @app = ShowExceptionsController.action(:boom) + test 'show diagnostics from a remote ip when env is already set' do + @app = ShowExceptionsController.action(:another_boom) self.remote_addr = '208.77.188.166' get '/' assert_match(/boom/, body) diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb index 7b734ff0fb..451ea6027d 100644 --- a/actionpack/test/controller/url_for_integration_test.rb +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -3,12 +3,6 @@ require 'abstract_unit' require 'controller/fake_controllers' require 'active_support/core_ext/object/with_options' -module RoutingTestHelpers - def url_for(set, options, recall = nil) - set.send(:url_for, options.merge(:only_path => true, :_path_segments => recall)) - end -end - module ActionPack class URLForIntegrationTest < ActiveSupport::TestCase include RoutingTestHelpers diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index dc07e07cb9..288efbf7c3 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -71,6 +71,14 @@ module AbstractController ) end + def test_subdomain_may_be_object + model = mock(:to_param => 'api') + add_host! + assert_equal('http://api.basecamphq.com/c/a/i', + W.new.url_for(:subdomain => model, :controller => 'c', :action => 'a', :id => 'i') + ) + end + def test_subdomain_may_be_removed add_host! assert_equal('http://basecamphq.com/c/a/i', diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 3765b7eb44..6ebd02e85c 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -564,100 +564,4 @@ class CookiesTest < ActionController::TestCase assert_not_equal expected.split("\n"), header end end -end - -class CookiesIntegrationTest < ActionDispatch::IntegrationTest - SessionKey = '_myapp_session' - SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33' - - class TestController < ActionController::Base - def dont_set_cookies - head :ok - end - - def set_cookies - cookies["that"] = "hello" - head :ok - end - end - - def test_setting_cookies_raises_after_stream_back_to_client - with_test_route_set do - get '/set_cookies' - assert_raise(ActionDispatch::ClosedError) { - request.cookie_jar['alert'] = 'alert' - cookies['alert'] = 'alert' - } - end - end - - def test_setting_cookies_raises_after_stream_back_to_client_even_without_cookies - with_test_route_set do - get '/dont_set_cookies' - assert_raise(ActionDispatch::ClosedError) { - request.cookie_jar['alert'] = 'alert' - } - end - end - - def test_setting_permanent_cookies_raises_after_stream_back_to_client - with_test_route_set do - get '/set_cookies' - assert_raise(ActionDispatch::ClosedError) { - request.cookie_jar.permanent['alert'] = 'alert' - cookies['alert'] = 'alert' - } - end - end - - def test_setting_permanent_cookies_raises_after_stream_back_to_client_even_without_cookies - with_test_route_set do - get '/dont_set_cookies' - assert_raise(ActionDispatch::ClosedError) { - request.cookie_jar.permanent['alert'] = 'alert' - } - end - end - - def test_setting_signed_cookies_raises_after_stream_back_to_client - with_test_route_set do - get '/set_cookies' - assert_raise(ActionDispatch::ClosedError) { - request.cookie_jar.signed['alert'] = 'alert' - cookies['alert'] = 'alert' - } - end - end - - def test_setting_signed_cookies_raises_after_stream_back_to_client_even_without_cookies - with_test_route_set do - get '/dont_set_cookies' - assert_raise(ActionDispatch::ClosedError) { - request.cookie_jar.signed['alert'] = 'alert' - } - end - end - - private - - # Overwrite get to send SessionSecret in env hash - def get(path, parameters = nil, env = {}) - env["action_dispatch.secret_token"] ||= SessionSecret - super - end - - def with_test_route_set - with_routing do |set| - set.draw do - match ':action', :to => CookiesIntegrationTest::TestController - end - - @app = self.class.build_app(set) do |middleware| - middleware.use ActionDispatch::Cookies - middleware.delete "ActionDispatch::ShowExceptions" - end - - yield - end - end -end +end
\ No newline at end of file diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb new file mode 100644 index 0000000000..f3dc160d7d --- /dev/null +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -0,0 +1,157 @@ +require 'abstract_unit' + +class DebugExceptionsTest < ActionDispatch::IntegrationTest + + class Boomer + attr_accessor :closed + + def initialize(detailed = false) + @detailed = detailed + @closed = false + end + + def each + end + + def close + @closed = true + end + + def call(env) + env['action_dispatch.show_detailed_exceptions'] = @detailed + req = ActionDispatch::Request.new(env) + case req.path + when "/pass" + [404, { "X-Cascade" => "pass" }, self] + when "/not_found" + raise ActionController::UnknownAction + when "/runtime_error" + raise RuntimeError + when "/method_not_allowed" + raise ActionController::MethodNotAllowed + when "/not_implemented" + raise ActionController::NotImplemented + when "/unprocessable_entity" + raise ActionController::InvalidAuthenticityToken + when "/not_found_original_exception" + raise ActionView::Template::Error.new('template', {}, AbstractController::ActionNotFound.new) + else + raise "puke!" + end + end + end + + ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false)) + DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true)) + + test 'skip diagnosis if not showing detailed exceptions' do + @app = ProductionApp + assert_raise RuntimeError do + get "/", {}, {'action_dispatch.show_exceptions' => true} + end + end + + test 'skip diagnosis if not showing exceptions' do + @app = DevelopmentApp + assert_raise RuntimeError do + get "/", {}, {'action_dispatch.show_exceptions' => false} + end + end + + test 'raise an exception on cascade pass' do + @app = ProductionApp + assert_raise ActionController::RoutingError do + get "/pass", {}, {'action_dispatch.show_exceptions' => true} + end + end + + test 'closes the response body on cascade pass' do + boomer = Boomer.new(false) + @app = ActionDispatch::DebugExceptions.new(boomer) + assert_raise ActionController::RoutingError do + get "/pass", {}, {'action_dispatch.show_exceptions' => true} + end + assert boomer.closed, "Expected to close the response body" + end + + test "rescue with diagnostics message" do + @app = DevelopmentApp + + get "/", {}, {'action_dispatch.show_exceptions' => true} + assert_response 500 + assert_match(/puke/, body) + + get "/not_found", {}, {'action_dispatch.show_exceptions' => true} + assert_response 404 + assert_match(/#{ActionController::UnknownAction.name}/, body) + + get "/method_not_allowed", {}, {'action_dispatch.show_exceptions' => true} + assert_response 405 + assert_match(/ActionController::MethodNotAllowed/, body) + end + + test "does not show filtered parameters" do + @app = DevelopmentApp + + get "/", {"foo"=>"bar"}, {'action_dispatch.show_exceptions' => true, + 'action_dispatch.parameter_filter' => [:foo]} + assert_response 500 + assert_match(""foo"=>"[FILTERED]"", body) + end + + test "show registered original exception for wrapped exceptions" do + @app = DevelopmentApp + + get "/not_found_original_exception", {}, {'action_dispatch.show_exceptions' => true} + assert_response 404 + assert_match(/AbstractController::ActionNotFound/, body) + end + + test "show the controller name in the diagnostics template when controller name is present" do + @app = DevelopmentApp + get("/runtime_error", {}, { + 'action_dispatch.show_exceptions' => true, + 'action_dispatch.request.parameters' => { + 'action' => 'show', + 'id' => 'unknown', + 'controller' => 'featured_tile' + } + }) + assert_response 500 + assert_match(/RuntimeError\n in FeaturedTileController/, body) + end + + test "sets the HTTP charset parameter" do + @app = DevelopmentApp + + get "/", {}, {'action_dispatch.show_exceptions' => true} + assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] + end + + test 'uses logger from env' do + @app = DevelopmentApp + output = StringIO.new + get "/", {}, {'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => Logger.new(output)} + assert_match(/puke/, output.rewind && output.read) + end + + test 'uses backtrace cleaner from env' do + @app = DevelopmentApp + cleaner = stub(:clean => ['passed backtrace cleaner']) + get "/", {}, {'action_dispatch.show_exceptions' => true, 'action_dispatch.backtrace_cleaner' => cleaner} + assert_match(/passed backtrace cleaner/, body) + end + + test 'logs exception backtrace when all lines silenced' do + output = StringIO.new + backtrace_cleaner = ActiveSupport::BacktraceCleaner.new + backtrace_cleaner.add_silencer { true } + + env = {'action_dispatch.show_exceptions' => true, + 'action_dispatch.logger' => Logger.new(output), + 'action_dispatch.backtrace_cleaner' => backtrace_cleaner} + + get "/", {}, env + assert_operator((output.rewind && output.read).lines.count, :>, 10) + end +end diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb index eaabc1feb3..3411bd14ea 100644 --- a/actionpack/test/dispatch/reloader_test.rb +++ b/actionpack/test/dispatch/reloader_test.rb @@ -43,6 +43,19 @@ class ReloaderTest < Test::Unit::TestCase assert_respond_to body, :close end + def test_condition_specifies_when_to_reload + i, j = 0, 0, 0, 0 + Reloader.to_prepare { |*args| i += 1 } + Reloader.to_cleanup { |*args| j += 1 } + app = Reloader.new(lambda { |env| [200, {}, []] }, lambda { i < 3 }) + 5.times do + resp = app.call({}) + resp[2].close + end + assert_equal 3, i + assert_equal 3, j + end + def test_returned_body_object_behaves_like_underlying_object body = call_and_return_body do b = MyBody.new @@ -116,6 +129,15 @@ class ReloaderTest < Test::Unit::TestCase assert cleaned end + def test_prepend_prepare_callback + i = 10 + Reloader.to_prepare { i += 1 } + Reloader.to_prepare(:prepend => true) { i = 0 } + + Reloader.prepare! + assert_equal 1, i + end + def test_cleanup_callbacks_are_called_on_exceptions cleaned = false Reloader.to_cleanup { cleaned = true } diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 5abbaf74fe..9bdd5ecbc6 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -5,6 +5,15 @@ class ResponseTest < ActiveSupport::TestCase @response = ActionDispatch::Response.new end + def test_response_body_encoding + # FIXME: remove this conditional on Rails 4.0 + return unless "<3".encoding_aware? + + body = ["hello".encode('utf-8')] + response = ActionDispatch::Response.new 200, {}, body + assert_equal Encoding::UTF_8, response.body.encoding + end + test "simple output" do @response.body = "Hello, World!" @@ -130,6 +139,17 @@ class ResponseTest < ActiveSupport::TestCase assert_equal('application/xml; charset=utf-16', resp.headers['Content-Type']) end + + test "read content type without charset" do + original = ActionDispatch::Response.default_charset + begin + ActionDispatch::Response.default_charset = 'utf-16' + resp = ActionDispatch::Response.new(200, { "Content-Type" => "text/xml" }) + assert_equal('utf-16', resp.charset) + ensure + ActionDispatch::Response.default_charset = original + end + end end class ResponseIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 19eee379fd..5325f81364 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -91,6 +91,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest match "/local/:action", :controller => "local" match "/projects/status(.:format)" + match "/404", :to => lambda { |env| [404, {"Content-Type" => "text/plain"}, ["NOT FOUND"]] } constraints(:ip => /192\.168\.1\.\d\d\d/) do get 'admin' => "queenbee#index" @@ -2296,7 +2297,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_named_routes_collision_is_avoided_unless_explicitly_given_as assert_equal "/c/1", routes_collision_path(1) - assert_equal "/forced_collision", routes_forced_collision_path + assert_equal "/fc", routes_forced_collision_path end def test_redirect_argument_error diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 5875725b5d..e9504f3524 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -3,24 +3,13 @@ require 'abstract_unit' class ShowExceptionsTest < ActionDispatch::IntegrationTest class Boomer - def initialize(detailed = false) - @detailed = detailed - end - def call(env) - env['action_dispatch.show_detailed_exceptions'] = @detailed req = ActionDispatch::Request.new(env) case req.path when "/not_found" raise ActionController::UnknownAction - when "/runtime_error" - raise RuntimeError when "/method_not_allowed" raise ActionController::MethodNotAllowed - when "/not_implemented" - raise ActionController::NotImplemented - when "/unprocessable_entity" - raise ActionController::InvalidAuthenticityToken when "/not_found_original_exception" raise ActionView::Template::Error.new('template', {}, AbstractController::ActionNotFound.new) else @@ -29,10 +18,16 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest end end - ProductionApp = ActionDispatch::ShowExceptions.new(Boomer.new(false)) - DevelopmentApp = ActionDispatch::ShowExceptions.new(Boomer.new(true)) + ProductionApp = ActionDispatch::ShowExceptions.new(Boomer.new, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")) - test "rescue with error page when show_exceptions is false" do + test "skip exceptions app if not showing exceptions" do + @app = ProductionApp + assert_raise RuntimeError do + get "/", {}, {'action_dispatch.show_exceptions' => false} + end + end + + test "rescue with error page" do @app = ProductionApp get "/", {}, {'action_dispatch.show_exceptions' => true} @@ -48,24 +43,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest assert_equal "", body end - test "rescue with diagnostics message when show_exceptions is true" do - @app = DevelopmentApp - - get "/", {}, {'action_dispatch.show_exceptions' => true} - assert_response 500 - assert_match(/puke/, body) - - get "/not_found", {}, {'action_dispatch.show_exceptions' => true} - assert_response 404 - assert_match(/#{ActionController::UnknownAction.name}/, body) - - get "/method_not_allowed", {}, {'action_dispatch.show_exceptions' => true} - assert_response 405 - assert_match(/ActionController::MethodNotAllowed/, body) - end - test "localize rescue error page" do - # Change locale old_locale, I18n.locale = I18n.locale, :da begin @@ -83,16 +61,14 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest end end - test "does not show filtered parameters" do - @app = DevelopmentApp + test "sets the HTTP charset parameter" do + @app = ProductionApp - get "/", {"foo"=>"bar"}, {'action_dispatch.show_exceptions' => true, - 'action_dispatch.parameter_filter' => [:foo]} - assert_response 500 - assert_match(""foo"=>"[FILTERED]"", body) + get "/", {}, {'action_dispatch.show_exceptions' => true} + assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] end - test "show registered original exception for wrapped exceptions when show_exceptions is false" do + test "show registered original exception for wrapped exceptions" do @app = ProductionApp get "/not_found_original_exception", {}, {'action_dispatch.show_exceptions' => true} @@ -100,39 +76,27 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest assert_match(/404 error/, body) end - test "show registered original exception for wrapped exceptions when show_exceptions is true" do - @app = DevelopmentApp + test "calls custom exceptions app" do + exceptions_app = lambda do |env| + assert_kind_of AbstractController::ActionNotFound, env["action_dispatch.exception"] + assert_equal "/404", env["PATH_INFO"] + [404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]] + end + @app = ActionDispatch::ShowExceptions.new(Boomer.new, exceptions_app) get "/not_found_original_exception", {}, {'action_dispatch.show_exceptions' => true} assert_response 404 - assert_match(/AbstractController::ActionNotFound/, body) + assert_equal "YOU FAILED BRO", body end - test "show the controller name in the diagnostics template when controller name is present" do - @app = DevelopmentApp - get("/runtime_error", {}, { - 'action_dispatch.show_exceptions' => true, - 'action_dispatch.request.parameters' => { - 'action' => 'show', - 'id' => 'unknown', - 'controller' => 'featured_tile' - } - }) - assert_response 500 - assert_match(/RuntimeError\n in FeaturedTileController/, body) - end - - test "sets the HTTP charset parameter" do - @app = DevelopmentApp - - get "/", {}, {'action_dispatch.show_exceptions' => true} - assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] - end + test "returns an empty response if custom exceptions app returns X-Cascade pass" do + exceptions_app = lambda do |env| + [404, { "X-Cascade" => "pass" }, []] + end - test 'uses logger from env' do - @app = ProductionApp - output = StringIO.new - get "/", {}, {'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => Logger.new(output)} - assert_match(/puke/, output.rewind && output.read) + @app = ActionDispatch::ShowExceptions.new(Boomer.new, exceptions_app) + get "/method_not_allowed", {}, {'action_dispatch.show_exceptions' => true} + assert_response 405 + assert_equal "", body end end diff --git a/actionpack/test/fixtures/company.rb b/actionpack/test/fixtures/company.rb index cd39ea7898..e29978801e 100644 --- a/actionpack/test/fixtures/company.rb +++ b/actionpack/test/fixtures/company.rb @@ -1,10 +1,10 @@ class Company < ActiveRecord::Base has_one :mascot attr_protected :rating - set_sequence_name :companies_nonstd_seq + self.sequence_name = :companies_nonstd_seq validates_presence_of :name def validate errors.add('rating', 'rating should not be 2') if rating == 2 end -end
\ No newline at end of file +end diff --git a/actionpack/test/fixtures/developer.rb b/actionpack/test/fixtures/developer.rb index c70eda34c6..dd14548fac 100644 --- a/actionpack/test/fixtures/developer.rb +++ b/actionpack/test/fixtures/developer.rb @@ -5,5 +5,5 @@ class Developer < ActiveRecord::Base end class DeVeLoPeR < ActiveRecord::Base - set_table_name "developers" + self.table_name = "developers" end diff --git a/actionpack/test/fixtures/test/_content_tag_nested_in_content_tag.erb b/actionpack/test/fixtures/test/_content_tag_nested_in_content_tag.erb new file mode 100644 index 0000000000..2f21a75dd9 --- /dev/null +++ b/actionpack/test/fixtures/test/_content_tag_nested_in_content_tag.erb @@ -0,0 +1,3 @@ +<%= content_tag 'p' do %> + <%= content_tag 'b', 'Hello' %> +<% end %> diff --git a/actionpack/test/template/compiled_templates_test.rb b/actionpack/test/template/compiled_templates_test.rb index 8fc78283d8..30d798d693 100644 --- a/actionpack/test/template/compiled_templates_test.rb +++ b/actionpack/test/template/compiled_templates_test.rb @@ -3,6 +3,10 @@ require 'controller/fake_models' class CompiledTemplatesTest < Test::Unit::TestCase def setup + # Clean up any details key cached to expose failures + # that otherwise would appear just on isolated tests + ActionView::LookupContext::DetailsKey.clear + @compiled_templates = ActionView::CompiledTemplates @compiled_templates.instance_methods.each do |m| @compiled_templates.send(:remove_method, m) if m =~ /^_render_template_/ diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index af30ec9892..fadfb59572 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -164,6 +164,15 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_day(nil, :include_blank => true) end + def test_select_day_with_two_digit_numbers + expected = %(<select id="date_day" name="date[day]">\n) + expected << %(<option value="1">01</option>\n<option selected="selected" value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8">08</option>\n<option value="9">09</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">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" + + assert_dom_equal expected, select_day(Time.mktime(2011, 8, 2), :use_two_digit_numbers => true) + assert_dom_equal expected, select_day(2, :use_two_digit_numbers => true) + end + def test_select_day_with_html_options expected = %(<select id="date_day" name="date[day]" class="selector">\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">15</option>\n<option value="16" selected="selected">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) @@ -198,6 +207,15 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_month(8) end + def test_select_month_with_two_digit_numbers + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value="1">01</option>\n<option value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8" selected="selected">08</option>\n<option value="9">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_month(Time.mktime(2011, 8, 16), :use_two_digit_numbers => true) + assert_dom_equal expected, select_month(8, :use_two_digit_numbers => true) + end + def test_select_month_with_disabled expected = %(<select id="date_month" name="date[month]" disabled="disabled">\n) expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) @@ -986,7 +1004,6 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :ampm => true) end - def test_select_datetime_with_separators expected = %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) @@ -1379,6 +1396,25 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on", :order => [ :month, :year ]) end + def test_date_select_without_day_with_separator + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n" + + 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", :date_separator => '/', :order => [ :month, :year ]) + end + def test_date_select_without_day_and_with_disabled_html_option @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 ad876df65d..73b936b16e 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -3,6 +3,8 @@ require 'controller/fake_models' require 'active_support/core_ext/object/inclusion' class FormHelperTest < ActionView::TestCase + include RenderERBUtils + tests ActionView::Helpers::FormHelper def form_for(*) @@ -231,9 +233,6 @@ class FormHelperTest < ActionView::TestCase end def test_label_with_block_in_erb - path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH) - view_paths = ActionView::PathSet.new([path]) - view = ActionView::Base.new(view_paths) assert_equal "<label for=\"post_message\">\n Message\n <input id=\"post_message\" name=\"post[message]\" size=\"30\" type=\"text\" />\n</label>", view.render("test/label_with_block") end @@ -690,6 +689,7 @@ class FormHelperTest < ActionView::TestCase concat f.text_area(:body) concat f.check_box(:secret) concat f.submit('Create post') + concat f.button('Create post') end expected = whole_form("/posts/123", "create-post" , "edit_post", :method => "put") do @@ -698,7 +698,8 @@ class FormHelperTest < ActionView::TestCase "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + - "<input name='commit' type='submit' value='Create post' />" + "<input name='commit' type='submit' value='Create post' />" + + "<button name='button' type='submit'>Create post</button>" end assert_dom_equal expected, output_buffer @@ -770,7 +771,7 @@ class FormHelperTest < ActionView::TestCase concat f.submit('Create post') end - expected = whole_form("/posts/123", "create-post", "other_name_edit", :method => "put") do + expected = whole_form("/posts/123", "create-post", "edit_other_name", :method => "put") do "<label for='other_name_title' class='post_title'>Title</label>" + "<input name='other_name[title]' size='30' id='other_name_title' value='Hello World' type='text' />" + "<textarea name='other_name[body]' id='other_name_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + @@ -907,7 +908,7 @@ class FormHelperTest < ActionView::TestCase concat f.check_box(:secret) end - expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'put') do "<label for='post_123_title'>Title</label>" + "<input name='post[123][title]' size='30' type='text' id='post_123_title' value='Hello World' />" + "<textarea name='post[123][body]' id='post_123_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + @@ -925,7 +926,7 @@ class FormHelperTest < ActionView::TestCase concat f.check_box(:secret) end - expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'put') do "<input name='post[][title]' size='30' type='text' id='post__title' value='Hello World' />" + "<textarea name='post[][body]' id='post__body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[][secret]' type='hidden' value='0' />" + @@ -1067,7 +1068,7 @@ class FormHelperTest < ActionView::TestCase concat f.submit end - expected = whole_form('/posts/123', 'another_post_edit', 'another_post_edit', :method => 'put') do + expected = whole_form('/posts/123', 'edit_another_post', 'edit_another_post', :method => 'put') do "<input name='commit' type='submit' value='Update your Post' />" end @@ -1099,7 +1100,7 @@ class FormHelperTest < ActionView::TestCase } end - expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'put') do "<input name='post[123][title]' size='30' type='text' id='post_123_title' value='Hello World' />" + "<input name='post[123][comment][][name]' size='30' type='text' id='post_123_comment__name' value='new comment' />" end @@ -1158,7 +1159,7 @@ class FormHelperTest < ActionView::TestCase } end - expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'put') do "<input name='post[123][comment][title]' size='30' type='text' id='post_123_comment_title' value='Hello World' />" end @@ -1186,7 +1187,7 @@ class FormHelperTest < ActionView::TestCase } end - expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'put') do "<input name='post[123][comment][123][title]' size='30' type='text' id='post_123_comment_123_title' value='Hello World' />" end @@ -1206,9 +1207,9 @@ class FormHelperTest < ActionView::TestCase } end - expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'put') do "<input name='post[123][comment][5][title]' size='30' type='text' id='post_123_comment_5_title' value='Hello World' />" - end + whole_form('/posts/123', 'post_edit', 'post_edit', 'put') do + end + whole_form('/posts/123', 'edit_post', 'edit_post', 'put') do "<input name='post[1][comment][123][title]' size='30' type='text' id='post_1_comment_123_title' value='Hello World' />" end @@ -1861,7 +1862,7 @@ class FormHelperTest < ActionView::TestCase } end - expected = whole_form('/posts/123', 'create-post', 'post_edit', :method => 'put') do + expected = whole_form('/posts/123', 'create-post', 'edit_post', :method => 'put') do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='parent_post[secret]' type='hidden' value='0' />" + @@ -1881,7 +1882,7 @@ class FormHelperTest < ActionView::TestCase } end - expected = whole_form('/posts/123', 'create-post', 'post_edit', :method => 'put') do + expected = whole_form('/posts/123', 'create-post', 'edit_post', :method => 'put') do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[comment][name]' type='text' id='post_comment_name' value='new comment' size='30' />" diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 6eae9bf846..233907d07a 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -2,6 +2,8 @@ require 'abstract_unit' require 'active_support/core_ext/object/inclusion' class FormTagHelperTest < ActionView::TestCase + include RenderERBUtils + tests ActionView::Helpers::FormTagHelper def setup @@ -104,14 +106,14 @@ class FormTagHelperTest < ActionView::TestCase end def test_form_tag_with_block_in_erb - output_buffer = form_tag("http://www.example.com") { concat "Hello world!" } + output_buffer = render_erb("<%= form_tag('http://www.example.com') do %>Hello world!<% end %>") expected = whole_form { "Hello world!" } assert_dom_equal expected, output_buffer end def test_form_tag_with_block_and_method_in_erb - output_buffer = form_tag("http://www.example.com", :method => :put) { concat "Hello world!" } + output_buffer = render_erb("<%= form_tag('http://www.example.com', :method => :put) do %>Hello world!<% end %>") expected = whole_form("http://www.example.com", :method => "put") do "Hello world!" @@ -485,27 +487,27 @@ class FormTagHelperTest < ActionView::TestCase end def test_field_set_tag_in_erb - output_buffer = field_set_tag("Your details") { concat "Hello world!" } + output_buffer = render_erb("<%= field_set_tag('Your details') do %>Hello world!<% end %>") expected = %(<fieldset><legend>Your details</legend>Hello world!</fieldset>) assert_dom_equal expected, output_buffer - output_buffer = field_set_tag { concat "Hello world!" } + output_buffer = render_erb("<%= field_set_tag do %>Hello world!<% end %>") expected = %(<fieldset>Hello world!</fieldset>) assert_dom_equal expected, output_buffer - output_buffer = field_set_tag('') { concat "Hello world!" } + output_buffer = render_erb("<%= field_set_tag('') do %>Hello world!<% end %>") expected = %(<fieldset>Hello world!</fieldset>) assert_dom_equal expected, output_buffer - output_buffer = field_set_tag('', :class => 'format') { concat "Hello world!" } + output_buffer = render_erb("<%= field_set_tag('', :class => 'format') do %>Hello world!<% end %>") expected = %(<fieldset class="format">Hello world!</fieldset>) assert_dom_equal expected, output_buffer end - + def test_text_area_tag_options_symbolize_keys_side_effects options = { :option => "random_option" } text_area_tag "body", "hello world", options diff --git a/actionpack/test/template/lookup_context_test.rb b/actionpack/test/template/lookup_context_test.rb index bac2530e3d..c65f707da0 100644 --- a/actionpack/test/template/lookup_context_test.rb +++ b/actionpack/test/template/lookup_context_test.rb @@ -1,20 +1,14 @@ require "abstract_unit" require "abstract_controller/rendering" -ActionView::LookupContext::DetailsKey.class_eval do - def self.details_keys - @details_keys - end -end - class LookupContextTest < ActiveSupport::TestCase def setup @lookup_context = ActionView::LookupContext.new(FIXTURE_LOAD_PATH, {}) + ActionView::LookupContext::DetailsKey.clear end def teardown I18n.locale = :en - ActionView::LookupContext::DetailsKey.details_keys.clear end test "process view paths on initialization" do diff --git a/actionpack/test/template/record_tag_helper_test.rb b/actionpack/test/template/record_tag_helper_test.rb index 7f23629e05..ec777d15c4 100644 --- a/actionpack/test/template/record_tag_helper_test.rb +++ b/actionpack/test/template/record_tag_helper_test.rb @@ -22,6 +22,8 @@ class Post end class RecordTagHelperTest < ActionView::TestCase + include RenderERBUtils + tests ActionView::Helpers::RecordTagHelper def setup @@ -58,13 +60,13 @@ class RecordTagHelperTest < ActionView::TestCase def test_block_works_with_content_tag_for_in_erb expected = %(<tr class="post" id="post_45">#{@post.body}</tr>) - actual = content_tag_for(:tr, @post) { concat @post.body } + actual = render_erb("<%= content_tag_for(:tr, @post) do %><%= @post.body %><% end %>") assert_dom_equal expected, actual end def test_div_for_in_erb expected = %(<div class="post bar" id="post_45">#{@post.body}</div>) - actual = div_for(@post, :class => "bar") { concat @post.body } + actual = render_erb("<%= div_for(@post, :class => 'bar') do %><%= @post.body %><% end %>") assert_dom_equal expected, actual end diff --git a/actionpack/test/template/sprockets_helper_test.rb b/actionpack/test/template/sprockets_helper_test.rb index 26a504beb8..64fdd53e73 100644 --- a/actionpack/test/template/sprockets_helper_test.rb +++ b/actionpack/test/template/sprockets_helper_test.rb @@ -41,6 +41,10 @@ class SprocketsHelperTest < ActionView::TestCase @controller ? @controller.config : @config end + def compute_host(source, request, options = {}) + raise "Should never get here" + end + test "asset_path" do assert_match %r{/assets/logo-[0-9a-f]+.png}, asset_path("logo.png") @@ -125,6 +129,10 @@ class SprocketsHelperTest < ActionView::TestCase assert_raises ActionController::RoutingError do asset_path("logo.png") end + @config.asset_host = method :compute_host + assert_raises ActionController::RoutingError do + asset_path("logo.png") + end end test "image_tag" do diff --git a/actionpack/test/template/sprockets_helper_with_routes_test.rb b/actionpack/test/template/sprockets_helper_with_routes_test.rb new file mode 100644 index 0000000000..bcbd81a7dd --- /dev/null +++ b/actionpack/test/template/sprockets_helper_with_routes_test.rb @@ -0,0 +1,57 @@ +require 'abstract_unit' +require 'sprockets' +require 'sprockets/helpers/rails_helper' +require 'mocha' + +class SprocketsHelperWithRoutesTest < ActionView::TestCase + include Sprockets::Helpers::RailsHelper + + # Let's bring in some named routes to test namespace conflicts with potential *_paths. + # We have to do this after we bring in the Sprockets RailsHelper so if there are conflicts, + # they'll fail in the way we expect in a real live Rails app. + routes = ActionDispatch::Routing::RouteSet.new + routes.draw do + resources :assets + end + include routes.url_helpers + + def setup + super + @controller = BasicController.new + + @assets = Sprockets::Environment.new + @assets.append_path(FIXTURES.join("sprockets/app/javascripts")) + @assets.append_path(FIXTURES.join("sprockets/app/stylesheets")) + @assets.append_path(FIXTURES.join("sprockets/app/images")) + + application = Struct.new(:config, :assets).new(config, @assets) + Rails.stubs(:application).returns(application) + @config = config + @config.perform_caching = true + @config.assets.digest = true + @config.assets.compile = true + end + + test "namespace conflicts on a named route called asset_path" do + # Testing this for sanity - asset_path is now a named route! + assert_match asset_path('test_asset'), '/assets/test_asset' + + assert_match %r{/assets/logo-[0-9a-f]+.png}, + path_to_asset("logo.png") + assert_match %r{/assets/logo-[0-9a-f]+.png}, + path_to_asset("logo.png", :digest => true) + assert_match %r{/assets/logo.png}, + path_to_asset("logo.png", :digest => false) + end + + test "javascript_include_tag with a named_route named asset_path" do + assert_match %r{<script src="/assets/application-[0-9a-f]+.js" type="text/javascript"></script>}, + javascript_include_tag(:application) + end + + test "stylesheet_link_tag with a named_route named asset_path" do + assert_match %r{<link href="/assets/application-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />}, + stylesheet_link_tag(:application) + end + +end
\ No newline at end of file diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb index 60b466a9ff..6c325d5abb 100644 --- a/actionpack/test/template/tag_helper_test.rb +++ b/actionpack/test/template/tag_helper_test.rb @@ -1,6 +1,8 @@ require 'abstract_unit' class TagHelperTest < ActionView::TestCase + include RenderERBUtils + tests ActionView::Helpers::TagHelper def test_tag @@ -44,12 +46,12 @@ class TagHelperTest < ActionView::TestCase end def test_content_tag_with_block_in_erb - buffer = content_tag(:div) { concat "Hello world!" } + buffer = render_erb("<%= content_tag(:div) do %>Hello world!<% end %>") assert_dom_equal "<div>Hello world!</div>", buffer end def test_content_tag_with_block_and_options_in_erb - buffer = content_tag(:div, :class => "green") { concat "Hello world!" } + buffer = render_erb("<%= content_tag(:div, :class => 'green') do %>Hello world!<% end %>") assert_dom_equal %(<div class="green">Hello world!</div>), buffer end @@ -68,10 +70,8 @@ class TagHelperTest < ActionView::TestCase output_buffer end - # TAG TODO: Move this into a real template def test_content_tag_nested_in_content_tag_in_erb - buffer = content_tag("p") { concat content_tag("b", "Hello") } - assert_equal '<p><b>Hello</b></p>', buffer + assert_equal "<p>\n <b>Hello</b>\n</p>", view.render("test/content_tag_nested_in_content_tag") end def test_content_tag_with_escaped_array_class diff --git a/actionpack/test/template/template_test.rb b/actionpack/test/template/template_test.rb index 70ca876c67..13d30a93ce 100644 --- a/actionpack/test/template/template_test.rb +++ b/actionpack/test/template/template_test.rb @@ -170,6 +170,7 @@ class TestERBTemplate < ActiveSupport::TestCase def with_external_encoding(encoding) old = Encoding.default_external + Encoding::Converter.new old, encoding if old != encoding silence_warnings { Encoding.default_external = encoding } yield ensure diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index caea0b86bd..bd9ed996fe 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,12 +1,9 @@ ## Rails 3.2.0 (unreleased) ## -* Renamed (with a deprecation the following constants): +* Deprecated `define_attr_method` in `ActiveModel::AttributeMethods`, because this only existed to + support methods like `set_table_name` in Active Record, which are themselves being deprecated. - ActiveModel::Serialization => ActiveModel::Serializable - ActiveModel::Serializers::JSON => ActiveModel::Serializable::JSON - ActiveModel::Serializers::Xml => ActiveModel::Serializable::XML - - *José Valim* + *Jon Leighton* * Add ActiveModel::Errors#added? to check if a specific error has been added *Martin Svalin* diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 7ea04344f0..d0e2a6f39c 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -43,7 +43,6 @@ module ActiveModel autoload :Observer, 'active_model/observing' autoload :Observing autoload :SecurePassword - autoload :Serializable autoload :Serialization autoload :TestCase autoload :Translation diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index e69cb5c459..dba059eacd 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -66,46 +66,29 @@ module ActiveModel end module ClassMethods - # Defines an "attribute" method (like +inheritance_column+ or +table_name+). - # A new (class) method will be created with the given name. If a value is - # specified, the new method will return that value (as a string). - # Otherwise, the given block will be used to compute the value of the - # method. - # - # The original method will be aliased, with the new name being prefixed - # with "original_". This allows the new method to access the original - # value. - # - # Example: - # - # class Person - # - # include ActiveModel::AttributeMethods - # - # cattr_accessor :primary_key - # cattr_accessor :inheritance_column - # - # define_attr_method :primary_key, "sysid" - # define_attr_method( :inheritance_column ) do - # original_inheritance_column + "_id" - # end - # - # end - # - # Provides you with: - # - # Person.primary_key - # # => "sysid" - # Person.inheritance_column = 'address' - # Person.inheritance_column - # # => 'address_id' - def define_attr_method(name, value=nil, &block) + def define_attr_method(name, value=nil, deprecation_warning = true, &block) #:nodoc: + # This deprecation_warning param is for internal use so that we can silence + # the warning from Active Record, because we are implementing more specific + # messages there instead. + # + # It doesn't apply to the original_#{name} method as we want to warn if + # people are calling that regardless. + if deprecation_warning + ActiveSupport::Deprecation.warn("define_attr_method is deprecated and will be removed without replacement.") + end + sing = singleton_class sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 - if method_defined?('original_#{name}') - undef :'original_#{name}' + remove_possible_method :'original_#{name}' + remove_possible_method :'_original_#{name}' + alias_method :'_original_#{name}', :'#{name}' + define_method :'original_#{name}' do + ActiveSupport::Deprecation.warn( + "This method is generated by ActiveModel::AttributeMethods::ClassMethods#define_attr_method, " \ + "which is deprecated and will be removed." + ) + send(:'_original_#{name}') end - alias_method :'original_#{name}', :'#{name}' eorb if block_given? sing.send :define_method, name, &block diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 0621a175bd..15103f1185 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -93,7 +93,7 @@ module ActiveModel :only => [:before, :around, :after] }.merge(options) - types = Array.wrap(options.delete(:only)) + types = Array.wrap(options.delete(:only)) callbacks.each do |callback| define_callbacks(callback, options) diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 953d24a3b2..755e54efcd 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -5,7 +5,9 @@ require 'active_support/core_ext/module/deprecation' module ActiveModel class Name < String - attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key, :i18n_key + attr_reader :singular, :plural, :element, :collection, :partial_path, + :singular_route_key, :route_key, :param_key, :i18n_key + alias_method :cache_key, :collection deprecate :partial_path => "ActiveModel::Name#partial_path is deprecated. Call #to_partial_path on model instances directly instead." @@ -26,8 +28,12 @@ module ActiveModel @collection = ActiveSupport::Inflector.tableize(self).freeze @partial_path = "#{@collection}/#{@element}".freeze @param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze - @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural).freeze @i18n_key = self.underscore.to_sym + + @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup) + @singular_route_key = ActiveSupport::Inflector.singularize(@route_key).freeze + @route_key << "_index" if @plural == @singular + @route_key.freeze end # Transform the model name into a more humane format, using I18n. By default, @@ -71,8 +77,8 @@ module ActiveModel # BookCover.model_name # => "BookCover" # BookCover.model_name.human # => "Book cover" # - # BookCover.model_name.i18n_key # => "book_cover" - # BookModule::BookCover.model_name.i18n_key # => "book_module.book_cover" + # BookCover.model_name.i18n_key # => :book_cover + # BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover" # # Providing the functionality that ActiveModel::Naming provides in your object # is required to pass the Active Model Lint test. So either extending the provided @@ -117,10 +123,25 @@ module ActiveModel # namespaced models regarding whether it's inside isolated engine. # # For isolated engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> post + # + # For shared engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> blog_post + def self.singular_route_key(record_or_class) + model_name_from_record_or_class(record_or_class).singular_route_key + end + + # Returns string to use while generating route names. It differs for + # namespaced models regarding whether it's inside isolated engine. + # + # For isolated engine: # ActiveModel::Naming.route_key(Blog::Post) #=> posts # # For shared engine: # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts + # + # The route key also considers if the noun is uncountable and, in + # such cases, automatically appends _index. def self.route_key(record_or_class) model_name_from_record_or_class(record_or_class).route_key end diff --git a/activemodel/lib/active_model/serializable.rb b/activemodel/lib/active_model/serializable.rb deleted file mode 100644 index 86770a25e4..0000000000 --- a/activemodel/lib/active_model/serializable.rb +++ /dev/null @@ -1,144 +0,0 @@ -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/string/inflections' - -module ActiveModel - # == Active Model Serializable - # - # Provides a basic serialization to a serializable_hash for your object. - # - # A minimal implementation could be: - # - # class Person - # - # include ActiveModel::Serializable - # - # attr_accessor :name - # - # def attributes - # {'name' => name} - # end - # - # end - # - # Which would provide you with: - # - # person = Person.new - # person.serializable_hash # => {"name"=>nil} - # person.name = "Bob" - # person.serializable_hash # => {"name"=>"Bob"} - # - # You need to declare some sort of attributes hash which contains the attributes - # you want to serialize and their current value. - # - # Most of the time though, you will want to include the JSON or XML - # serializations. Both of these modules automatically include the - # ActiveModel::Serialization module, so there is no need to explicitly - # include it. - # - # So a minimal implementation including XML and JSON would be: - # - # class Person - # - # include ActiveModel::Serializable::JSON - # include ActiveModel::Serializable::XML - # - # attr_accessor :name - # - # def attributes - # {'name' => name} - # end - # - # end - # - # Which would provide you with: - # - # person = Person.new - # person.serializable_hash # => {"name"=>nil} - # person.as_json # => {"name"=>nil} - # person.to_json # => "{\"name\":null}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... - # - # person.name = "Bob" - # person.serializable_hash # => {"name"=>"Bob"} - # person.as_json # => {"name"=>"Bob"} - # person.to_json # => "{\"name\":\"Bob\"}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... - # - # Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> . - module Serializable - extend ActiveSupport::Concern - - autoload :JSON, "active_model/serializable/json" - autoload :XML, "active_model/serializable/xml" - - def serializable_hash(options = nil) - options ||= {} - - attribute_names = attributes.keys.sort - if only = options[:only] - attribute_names &= Array.wrap(only).map(&:to_s) - elsif except = options[:except] - attribute_names -= Array.wrap(except).map(&:to_s) - end - - hash = {} - attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } - - method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) } - method_names.each { |n| hash[n] = send(n) } - - serializable_add_includes(options) do |association, records, opts| - hash[association] = if records.is_a?(Enumerable) - records.map { |a| a.serializable_hash(opts) } - else - records.serializable_hash(opts) - end - end - - hash - end - - private - - # Hook method defining how an attribute value should be retrieved for - # serialization. By default this is assumed to be an instance named after - # the attribute. Override this method in subclasses should you need to - # retrieve the value for a given attribute differently: - # - # class MyClass - # include ActiveModel::Validations - # - # def initialize(data = {}) - # @data = data - # end - # - # def read_attribute_for_serialization(key) - # @data[key] - # end - # end - # - alias :read_attribute_for_serialization :send - - # Add associations specified via the <tt>:include</tt> option. - # - # Expects a block that takes as arguments: - # +association+ - name of the association - # +records+ - the association record(s) to be serialized - # +opts+ - options for the association records - def serializable_add_includes(options = {}) #:nodoc: - return unless include = options[:include] - - unless include.is_a?(Hash) - include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] - end - - include.each do |association, opts| - if records = send(association) - yield association, records, opts - end - end - end - end -end diff --git a/activemodel/lib/active_model/serializable/json.rb b/activemodel/lib/active_model/serializable/json.rb deleted file mode 100644 index 79173929e4..0000000000 --- a/activemodel/lib/active_model/serializable/json.rb +++ /dev/null @@ -1,108 +0,0 @@ -require 'active_support/json' -require 'active_support/core_ext/class/attribute' - -module ActiveModel - # == Active Model Serializable as JSON - module Serializable - module JSON - extend ActiveSupport::Concern - include ActiveModel::Serializable - - included do - extend ActiveModel::Naming - - class_attribute :include_root_in_json - self.include_root_in_json = true - end - - # Returns a hash representing the model. Some configuration can be - # passed through +options+. - # - # The option <tt>include_root_in_json</tt> controls the top-level behavior - # of +as_json+. If true (the default) +as_json+ will emit a single root - # node named after the object's type. For example: - # - # user = User.find(1) - # user.as_json - # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} } - # - # ActiveRecord::Base.include_root_in_json = false - # user.as_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in: - # - # user = User.find(1) - # user.as_json(root: false) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # The remainder of the examples in this section assume include_root_in_json is set to - # <tt>false</tt>. - # - # Without any +options+, the returned Hash will include all the model's - # attributes. For example: - # - # user = User.find(1) - # user.as_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes - # included, and work similar to the +attributes+ method. For example: - # - # user.as_json(:only => [ :id, :name ]) - # # => {"id": 1, "name": "Konata Izumi"} - # - # user.as_json(:except => [ :id, :created_at, :age ]) - # # => {"name": "Konata Izumi", "awesome": true} - # - # To include the result of some method calls on the model use <tt>:methods</tt>: - # - # user.as_json(:methods => :permalink) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "permalink": "1-konata-izumi"} - # - # To include associations use <tt>:include</tt>: - # - # user.as_json(:include => :posts) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, - # {"id": 2, author_id: 1, "title": "So I was thinking"}]} - # - # Second level and higher order associations work as well: - # - # user.as_json(:include => { :posts => { - # :include => { :comments => { - # :only => :body } }, - # :only => :title } }) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}], - # "title": "Welcome to the weblog"}, - # {"comments": [{"body": "Don't think too hard"}], - # "title": "So I was thinking"}]} - def as_json(options = nil) - root = include_root_in_json - root = options[:root] if options.try(:key?, :root) - if root - root = self.class.model_name.element if root == true - { root => serializable_hash(options) } - else - serializable_hash(options) - end - end - - def from_json(json, include_root=include_root_in_json) - hash = ActiveSupport::JSON.decode(json) - hash = hash.values.first if include_root - self.attributes = hash - self - end - end - end -end diff --git a/activemodel/lib/active_model/serializable/xml.rb b/activemodel/lib/active_model/serializable/xml.rb deleted file mode 100644 index d11cee9b42..0000000000 --- a/activemodel/lib/active_model/serializable/xml.rb +++ /dev/null @@ -1,195 +0,0 @@ -require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/core_ext/array/conversions' -require 'active_support/core_ext/hash/conversions' -require 'active_support/core_ext/hash/slice' - -module ActiveModel - # == Active Model Serializable as XML - module Serializable - module XML - extend ActiveSupport::Concern - include ActiveModel::Serializable - - class Serializer #:nodoc: - class Attribute #:nodoc: - attr_reader :name, :value, :type - - def initialize(name, serializable, value) - @name, @serializable = name, serializable - value = value.in_time_zone if value.respond_to?(:in_time_zone) - @value = value - @type = compute_type - end - - def decorations - decorations = {} - decorations[:encoding] = 'base64' if type == :binary - decorations[:type] = (type == :string) ? nil : type - decorations[:nil] = true if value.nil? - decorations - end - - protected - - def compute_type - return if value.nil? - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] - type ||= :string if value.respond_to?(:to_str) - type ||= :yaml - type - end - end - - class MethodAttribute < Attribute #:nodoc: - end - - attr_reader :options - - def initialize(serializable, options = nil) - @serializable = serializable - @options = options ? options.dup : {} - end - - def serializable_hash - @serializable.serializable_hash(@options.except(:include)) - end - - def serializable_collection - methods = Array.wrap(options[:methods]).map(&:to_s) - serializable_hash.map do |name, value| - name = name.to_s - if methods.include?(name) - self.class::MethodAttribute.new(name, @serializable, value) - else - self.class::Attribute.new(name, @serializable, value) - end - end - end - - def serialize - require 'builder' unless defined? ::Builder - - options[:indent] ||= 2 - options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) - - @builder = options[:builder] - @builder.instruct! unless options[:skip_instruct] - - root = (options[:root] || @serializable.class.model_name.element).to_s - root = ActiveSupport::XmlMini.rename_key(root, options) - - args = [root] - args << {:xmlns => options[:namespace]} if options[:namespace] - args << {:type => options[:type]} if options[:type] && !options[:skip_types] - - @builder.tag!(*args) do - add_attributes_and_methods - add_includes - add_extra_behavior - add_procs - yield @builder if block_given? - end - end - - private - - def add_extra_behavior - end - - def add_attributes_and_methods - serializable_collection.each do |attribute| - key = ActiveSupport::XmlMini.rename_key(attribute.name, options) - ActiveSupport::XmlMini.to_tag(key, attribute.value, - options.merge(attribute.decorations)) - end - end - - def add_includes - @serializable.send(:serializable_add_includes, options) do |association, records, opts| - add_associations(association, records, opts) - end - end - - # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. - def add_associations(association, records, opts) - merged_options = opts.merge(options.slice(:builder, :indent)) - merged_options[:skip_instruct] = true - - if records.is_a?(Enumerable) - tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) - type = options[:skip_types] ? { } : {:type => "array"} - association_name = association.to_s.singularize - merged_options[:root] = association_name - - if records.empty? - @builder.tag!(tag, type) - else - @builder.tag!(tag, type) do - records.each do |record| - if options[:skip_types] - record_type = {} - else - record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name - record_type = {:type => record_class} - end - - record.to_xml merged_options.merge(record_type) - end - end - end - else - merged_options[:root] = association.to_s - records.to_xml(merged_options) - end - end - - def add_procs - if procs = options.delete(:procs) - Array.wrap(procs).each do |proc| - if proc.arity == 1 - proc.call(options) - else - proc.call(options, @serializable) - end - end - end - end - end - - # Returns XML representing the model. Configuration can be - # passed through +options+. - # - # Without any +options+, the returned XML string will include all the model's - # attributes. For example: - # - # user = User.find(1) - # user.to_xml - # - # <?xml version="1.0" encoding="UTF-8"?> - # <user> - # <id type="integer">1</id> - # <name>David</name> - # <age type="integer">16</age> - # <created-at type="datetime">2011-01-30T22:29:23Z</created-at> - # </user> - # - # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes - # included, and work similar to the +attributes+ method. - # - # To include the result of some method calls on the model use <tt>:methods</tt>. - # - # To include associations use <tt>:include</tt>. - # - # For further documentation see activerecord/lib/active_record/serializers/xml_serializer.xml. - def to_xml(options = {}, &block) - Serializer.new(self, options).serialize(&block) - end - - def from_xml(xml) - self.attributes = Hash.from_xml(xml).values.first - self - end - end - end -end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 439302c632..a4b58ab456 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,10 +1,139 @@ +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/array/wrap' + + module ActiveModel + # == Active Model Serialization + # + # Provides a basic serialization to a serializable_hash for your object. + # + # A minimal implementation could be: + # + # class Person + # + # include ActiveModel::Serialization + # + # attr_accessor :name + # + # def attributes + # {'name' => name} + # end + # + # end + # + # Which would provide you with: + # + # person = Person.new + # person.serializable_hash # => {"name"=>nil} + # person.name = "Bob" + # person.serializable_hash # => {"name"=>"Bob"} + # + # You need to declare some sort of attributes hash which contains the attributes + # you want to serialize and their current value. + # + # Most of the time though, you will want to include the JSON or XML + # serializations. Both of these modules automatically include the + # ActiveModel::Serialization module, so there is no need to explicitly + # include it. + # + # So a minimal implementation including XML and JSON would be: + # + # class Person + # + # include ActiveModel::Serializers::JSON + # include ActiveModel::Serializers::Xml + # + # attr_accessor :name + # + # def attributes + # {'name' => name} + # end + # + # end + # + # Which would provide you with: + # + # person = Person.new + # person.serializable_hash # => {"name"=>nil} + # person.as_json # => {"name"=>nil} + # person.to_json # => "{\"name\":null}" + # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... + # + # person.name = "Bob" + # person.serializable_hash # => {"name"=>"Bob"} + # person.as_json # => {"name"=>"Bob"} + # person.to_json # => "{\"name\":\"Bob\"}" + # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... + # + # Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> . module Serialization - extend ActiveSupport::Concern - include ActiveModel::Serializable + def serializable_hash(options = nil) + options ||= {} + + attribute_names = attributes.keys.sort + if only = options[:only] + attribute_names &= Array.wrap(only).map(&:to_s) + elsif except = options[:except] + attribute_names -= Array.wrap(except).map(&:to_s) + end + + hash = {} + attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } + + method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) } + method_names.each { |n| hash[n] = send(n) } + + serializable_add_includes(options) do |association, records, opts| + hash[association] = if records.is_a?(Enumerable) + records.map { |a| a.serializable_hash(opts) } + else + records.serializable_hash(opts) + end + end - included do - ActiveSupport::Deprecation.warn "ActiveModel::Serialization is deprecated in favor of ActiveModel::Serializable" + hash end + + private + + # Hook method defining how an attribute value should be retrieved for + # serialization. By default this is assumed to be an instance named after + # the attribute. Override this method in subclasses should you need to + # retrieve the value for a given attribute differently: + # + # class MyClass + # include ActiveModel::Validations + # + # def initialize(data = {}) + # @data = data + # end + # + # def read_attribute_for_serialization(key) + # @data[key] + # end + # end + # + alias :read_attribute_for_serialization :send + + # Add associations specified via the <tt>:include</tt> option. + # + # Expects a block that takes as arguments: + # +association+ - name of the association + # +records+ - the association record(s) to be serialized + # +opts+ - options for the association records + def serializable_add_includes(options = {}) #:nodoc: + return unless include = options[:include] + + unless include.is_a?(Hash) + include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] + end + + include.each do |association, opts| + if records = send(association) + yield association, records, opts + end + end + end end -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb deleted file mode 100644 index 0e23df2f2b..0000000000 --- a/activemodel/lib/active_model/serializer.rb +++ /dev/null @@ -1,253 +0,0 @@ -require "active_support/core_ext/class/attribute" -require "active_support/core_ext/string/inflections" -require "active_support/core_ext/module/anonymous" -require "set" - -module ActiveModel - # Active Model Array Serializer - # - # It serializes an array checking if each element that implements - # the +active_model_serializer+ method passing down the current scope. - class ArraySerializer - attr_reader :object, :scope - - def initialize(object, scope) - @object, @scope = object, scope - end - - def serializable_array - @object.map do |item| - if item.respond_to?(:active_model_serializer) && (serializer = item.active_model_serializer) - serializer.new(item, scope) - else - item - end - end - end - - def as_json(*args) - serializable_array.as_json(*args) - end - end - - # Active Model Serializer - # - # Provides a basic serializer implementation that allows you to easily - # control how a given object is going to be serialized. On initialization, - # it expects to object as arguments, a resource and a scope. For example, - # one may do in a controller: - # - # PostSerializer.new(@post, current_user).to_json - # - # The object to be serialized is the +@post+ and the scope is +current_user+. - # - # We use the scope to check if a given attribute should be serialized or not. - # For example, some attributes maybe only be returned if +current_user+ is the - # author of the post: - # - # class PostSerializer < ActiveModel::Serializer - # attributes :title, :body - # has_many :comments - # - # private - # - # def attributes - # hash = super - # hash.merge!(:email => post.email) if author? - # hash - # end - # - # def author? - # post.author == scope - # end - # end - # - class Serializer - module Associations #:nodoc: - class Config < Struct.new(:name, :options) #:nodoc: - def serializer - options[:serializer] - end - end - - class HasMany < Config #:nodoc: - def serialize(collection, scope) - collection.map do |item| - serializer.new(item, scope).serializable_hash - end - end - - def serialize_ids(collection, scope) - # use named scopes if they are present - # return collection.ids if collection.respond_to?(:ids) - - collection.map do |item| - item.read_attribute_for_serialization(:id) - end - end - end - - class HasOne < Config #:nodoc: - def serialize(object, scope) - object && serializer.new(object, scope).serializable_hash - end - - def serialize_ids(object, scope) - object && object.read_attribute_for_serialization(:id) - end - end - end - - class_attribute :_attributes - self._attributes = Set.new - - class_attribute :_associations - self._associations = [] - - class_attribute :_root - class_attribute :_embed - self._embed = :objects - class_attribute :_root_embed - - class << self - # Define attributes to be used in the serialization. - def attributes(*attrs) - self._attributes += attrs - end - - def associate(klass, attrs) #:nodoc: - options = attrs.extract_options! - self._associations += attrs.map do |attr| - unless method_defined?(attr) - class_eval "def #{attr}() object.#{attr} end", __FILE__, __LINE__ - end - - options[:serializer] ||= const_get("#{attr.to_s.camelize}Serializer") - klass.new(attr, options) - end - end - - # Defines an association in the object should be rendered. - # - # The serializer object should implement the association name - # as a method which should return an array when invoked. If a method - # with the association name does not exist, the association name is - # dispatched to the serialized object. - def has_many(*attrs) - associate(Associations::HasMany, attrs) - end - - # Defines an association in the object should be rendered. - # - # The serializer object should implement the association name - # as a method which should return an object when invoked. If a method - # with the association name does not exist, the association name is - # dispatched to the serialized object. - def has_one(*attrs) - associate(Associations::HasOne, attrs) - end - - # Define how associations should be embedded. - # - # embed :objects # Embed associations as full objects - # embed :ids # Embed only the association ids - # embed :ids, :include => true # Embed the association ids and include objects in the root - # - def embed(type, options={}) - self._embed = type - self._root_embed = true if options[:include] - end - - # Defines the root used on serialization. If false, disables the root. - def root(name) - self._root = name - end - - def inherited(klass) #:nodoc: - return if klass.anonymous? - - name = klass.name.demodulize.underscore.sub(/_serializer$/, '') - - klass.class_eval do - alias_method name.to_sym, :object - root name.to_sym unless self._root == false - end - end - end - - attr_reader :object, :scope - - def initialize(object, scope) - @object, @scope = object, scope - end - - # Returns a json representation of the serializable - # object including the root. - def as_json(*) - if _root - hash = { _root => serializable_hash } - hash.merge!(associations) if _root_embed - hash - else - serializable_hash - end - end - - # Returns a hash representation of the serializable - # object without the root. - def serializable_hash - if _embed == :ids - attributes.merge(association_ids) - elsif _embed == :objects - attributes.merge(associations) - else - attributes - end - end - - # Returns a hash representation of the serializable - # object associations. - def associations - hash = {} - - _associations.each do |association| - associated_object = send(association.name) - hash[association.name] = association.serialize(associated_object, scope) - end - - hash - end - - # Returns a hash representation of the serializable - # object associations ids. - def association_ids - hash = {} - - _associations.each do |association| - associated_object = send(association.name) - hash[association.name] = association.serialize_ids(associated_object, scope) - end - - hash - end - - # Returns a hash representation of the serializable - # object attributes. - def attributes - hash = {} - - _attributes.each do |name| - hash[name] = @object.read_attribute_for_serialization(name) - end - - hash - end - end -end - -class Array - # Array uses ActiveModel::ArraySerializer. - def active_model_serializer - ActiveModel::ArraySerializer - end -end
\ No newline at end of file diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 9efd7c5f69..c845440120 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -1,12 +1,108 @@ +require 'active_support/json' +require 'active_support/core_ext/class/attribute' + module ActiveModel + # == Active Model JSON Serializer module Serializers module JSON extend ActiveSupport::Concern - include ActiveModel::Serializable::JSON + include ActiveModel::Serialization included do - ActiveSupport::Deprecation.warn "ActiveModel::Serializers::JSON is deprecated in favor of ActiveModel::Serializable::JSON" + extend ActiveModel::Naming + + class_attribute :include_root_in_json + self.include_root_in_json = true + end + + # Returns a hash representing the model. Some configuration can be + # passed through +options+. + # + # The option <tt>include_root_in_json</tt> controls the top-level behavior + # of +as_json+. If true (the default) +as_json+ will emit a single root + # node named after the object's type. For example: + # + # user = User.find(1) + # user.as_json + # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} } + # + # ActiveRecord::Base.include_root_in_json = false + # user.as_json + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} + # + # This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in: + # + # user = User.find(1) + # user.as_json(root: false) + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} + # + # The remainder of the examples in this section assume include_root_in_json is set to + # <tt>false</tt>. + # + # Without any +options+, the returned Hash will include all the model's + # attributes. For example: + # + # user = User.find(1) + # user.as_json + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} + # + # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes + # included, and work similar to the +attributes+ method. For example: + # + # user.as_json(:only => [ :id, :name ]) + # # => {"id": 1, "name": "Konata Izumi"} + # + # user.as_json(:except => [ :id, :created_at, :age ]) + # # => {"name": "Konata Izumi", "awesome": true} + # + # To include the result of some method calls on the model use <tt>:methods</tt>: + # + # user.as_json(:methods => :permalink) + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true, + # "permalink": "1-konata-izumi"} + # + # To include associations use <tt>:include</tt>: + # + # user.as_json(:include => :posts) + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true, + # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, + # {"id": 2, author_id: 1, "title": "So I was thinking"}]} + # + # Second level and higher order associations work as well: + # + # user.as_json(:include => { :posts => { + # :include => { :comments => { + # :only => :body } }, + # :only => :title } }) + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true, + # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}], + # "title": "Welcome to the weblog"}, + # {"comments": [{"body": "Don't think too hard"}], + # "title": "So I was thinking"}]} + def as_json(options = nil) + root = include_root_in_json + root = options[:root] if options.try(:key?, :root) + if root + root = self.class.model_name.element if root == true + { root => serializable_hash(options) } + else + serializable_hash(options) + end + end + + def from_json(json, include_root=include_root_in_json) + hash = ActiveSupport::JSON.decode(json) + hash = hash.values.first if include_root + self.attributes = hash + self end end end -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 620390da6b..d61d9d7119 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -1,14 +1,195 @@ +require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/array/conversions' +require 'active_support/core_ext/hash/conversions' +require 'active_support/core_ext/hash/slice' + module ActiveModel + # == Active Model XML Serializer module Serializers module Xml extend ActiveSupport::Concern - include ActiveModel::Serializable::XML + include ActiveModel::Serialization + + class Serializer #:nodoc: + class Attribute #:nodoc: + attr_reader :name, :value, :type + + def initialize(name, serializable, value) + @name, @serializable = name, serializable + value = value.in_time_zone if value.respond_to?(:in_time_zone) + @value = value + @type = compute_type + end + + def decorations + decorations = {} + decorations[:encoding] = 'base64' if type == :binary + decorations[:type] = (type == :string) ? nil : type + decorations[:nil] = true if value.nil? + decorations + end + + protected + + def compute_type + return if value.nil? + type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] + type ||= :string if value.respond_to?(:to_str) + type ||= :yaml + type + end + end + + class MethodAttribute < Attribute #:nodoc: + end + + attr_reader :options + + def initialize(serializable, options = nil) + @serializable = serializable + @options = options ? options.dup : {} + end + + def serializable_hash + @serializable.serializable_hash(@options.except(:include)) + end + + def serializable_collection + methods = Array.wrap(options[:methods]).map(&:to_s) + serializable_hash.map do |name, value| + name = name.to_s + if methods.include?(name) + self.class::MethodAttribute.new(name, @serializable, value) + else + self.class::Attribute.new(name, @serializable, value) + end + end + end + + def serialize + require 'builder' unless defined? ::Builder + + options[:indent] ||= 2 + options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) + + @builder = options[:builder] + @builder.instruct! unless options[:skip_instruct] - Serializer = ActiveModel::Serializable::XML::Serializer + root = (options[:root] || @serializable.class.model_name.element).to_s + root = ActiveSupport::XmlMini.rename_key(root, options) + + args = [root] + args << {:xmlns => options[:namespace]} if options[:namespace] + args << {:type => options[:type]} if options[:type] && !options[:skip_types] + + @builder.tag!(*args) do + add_attributes_and_methods + add_includes + add_extra_behavior + add_procs + yield @builder if block_given? + end + end + + private + + def add_extra_behavior + end + + def add_attributes_and_methods + serializable_collection.each do |attribute| + key = ActiveSupport::XmlMini.rename_key(attribute.name, options) + ActiveSupport::XmlMini.to_tag(key, attribute.value, + options.merge(attribute.decorations)) + end + end + + def add_includes + @serializable.send(:serializable_add_includes, options) do |association, records, opts| + add_associations(association, records, opts) + end + end + + # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. + def add_associations(association, records, opts) + merged_options = opts.merge(options.slice(:builder, :indent)) + merged_options[:skip_instruct] = true + + if records.is_a?(Enumerable) + tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) + type = options[:skip_types] ? { } : {:type => "array"} + association_name = association.to_s.singularize + merged_options[:root] = association_name + + if records.empty? + @builder.tag!(tag, type) + else + @builder.tag!(tag, type) do + records.each do |record| + if options[:skip_types] + record_type = {} + else + record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name + record_type = {:type => record_class} + end + + record.to_xml merged_options.merge(record_type) + end + end + end + else + merged_options[:root] = association.to_s + records.to_xml(merged_options) + end + end + + def add_procs + if procs = options.delete(:procs) + Array.wrap(procs).each do |proc| + if proc.arity == 1 + proc.call(options) + else + proc.call(options, @serializable) + end + end + end + end + end + + # Returns XML representing the model. Configuration can be + # passed through +options+. + # + # Without any +options+, the returned XML string will include all the model's + # attributes. For example: + # + # user = User.find(1) + # user.to_xml + # + # <?xml version="1.0" encoding="UTF-8"?> + # <user> + # <id type="integer">1</id> + # <name>David</name> + # <age type="integer">16</age> + # <created-at type="datetime">2011-01-30T22:29:23Z</created-at> + # </user> + # + # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes + # included, and work similar to the +attributes+ method. + # + # To include the result of some method calls on the model use <tt>:methods</tt>. + # + # To include associations use <tt>:include</tt>. + # + # For further documentation see activerecord/lib/active_record/serializers/xml_serializer.xml. + def to_xml(options = {}, &block) + Serializer.new(self, options).serialize(&block) + end - included do - ActiveSupport::Deprecation.warn "ActiveModel::Serializers::Xml is deprecated in favor of ActiveModel::Serializable::XML" + def from_xml(xml) + self.attributes = Hash.from_xml(xml).values.first + self end end end -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 6d64c81b5f..02b7c54d61 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -43,13 +43,25 @@ module ActiveModel # # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) - defaults = lookup_ancestors.map do |klass| - :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}" + defaults = [] + parts = attribute.to_s.split(".", 2) + attribute = parts.pop + namespace = parts.pop + + if namespace + lookup_ancestors.each do |klass| + defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}" + end + defaults << :"#{self.i18n_scope}.attributes.#{namespace}.#{attribute}" + else + lookup_ancestors.each do |klass| + defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}" + end end defaults << :"attributes.#{attribute}" defaults << options.delete(:default) if options[:default] - defaults << attribute.to_s.humanize + defaults << attribute.humanize options.reverse_merge! :count => 1, :default => defaults I18n.translate(defaults.shift, options) diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index eb7aac709d..a38de27b3c 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/string/encoding" + module ActiveModel # == Active Model Length Validator @@ -6,7 +8,6 @@ module ActiveModel MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze - DEFAULT_TOKENIZER = lambda { |value| value.split(//) } RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long] def initialize(options) @@ -23,7 +24,7 @@ module ActiveModel keys = CHECKS.keys & options.keys if keys.empty? - raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' + raise ArgumentError, 'Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option.' end keys.each do |key| @@ -36,14 +37,11 @@ module ActiveModel end def validate_each(record, attribute, value) - value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.kind_of?(String) + value = tokenize(value) + value_length = value.respond_to?(:length) ? value.length : value.to_s.length CHECKS.each do |key, validity_check| next unless check_value = options[key] - - value ||= [] if key == :maximum - - value_length = value.respond_to?(:length) ? value.length : value.to_s.length next if value_length.send(validity_check, check_value) errors_options = options.except(*RESERVED_OPTIONS) @@ -55,6 +53,18 @@ module ActiveModel record.errors.add(attribute, MESSAGES[key], errors_options) end end + + private + + def tokenize(value) + if value.kind_of?(String) + if options[:tokenizer] + options[:tokenizer].call(value) + elsif !value.encoding_aware? + value.mb_chars + end + end || value + end end module HelperMethods @@ -96,7 +106,7 @@ module ActiveModel # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to # count words as in above example.) # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. - # * <tt>:strict</tt> - Specifies whether validation should be strict. + # * <tt>:strict</tt> - Specifies whether validation should be strict. # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 35af7152db..9a643a6f5c 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -25,14 +25,14 @@ module ActiveModel # This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>. # # Configuration options: - # * <tt>message</tt> - A custom error message (default is: "can't be blank"). + # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. - # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. - # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 93a340eb39..72b8562b93 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -56,7 +56,7 @@ module ActiveModel # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. - # * <tt>unless</tt> - Specifies a method, proc or string to call to + # * <tt>:unless</tt> - Specifies a method, proc or string to call to # determine if the validation should not occur # (e.g. <tt>:unless => :skip_validation</tt>, or # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index 67471ed497..90f9b78334 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -134,20 +134,33 @@ class AttributeMethodsTest < ActiveModel::TestCase end test '#define_attr_method generates attribute method' do - ModelWithAttributes.define_attr_method(:bar, 'bar') + assert_deprecated do + ModelWithAttributes.define_attr_method(:bar, 'bar') + end assert_respond_to ModelWithAttributes, :bar - assert_equal "original bar", ModelWithAttributes.original_bar + + assert_deprecated do + assert_equal "original bar", ModelWithAttributes.original_bar + end + assert_equal "bar", ModelWithAttributes.bar - ModelWithAttributes.define_attr_method(:bar) + ActiveSupport::Deprecation.silence do + ModelWithAttributes.define_attr_method(:bar) + end assert !ModelWithAttributes.bar end test '#define_attr_method generates attribute method with invalid identifier characters' do - ModelWithWeirdNamesAttributes.define_attr_method(:'c?d', 'c?d') + ActiveSupport::Deprecation.silence do + ModelWithWeirdNamesAttributes.define_attr_method(:'c?d', 'c?d') + end assert_respond_to ModelWithWeirdNamesAttributes, :'c?d' - assert_equal "original c?d", ModelWithWeirdNamesAttributes.send('original_c?d') + + ActiveSupport::Deprecation.silence do + assert_equal "original c?d", ModelWithWeirdNamesAttributes.send('original_c?d') + end assert_equal "c?d", ModelWithWeirdNamesAttributes.send('c?d') end diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index e8db73ba52..acda989eec 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -34,6 +34,10 @@ class NamingTest < ActiveModel::TestCase def test_human assert_equal 'Track back', @model_name.human end + + def test_i18n_key + assert_equal :"post/track_back", @model_name.i18n_key + end end class NamingWithNamespacedModelInIsolatedNamespaceTest < ActiveModel::TestCase @@ -74,6 +78,10 @@ class NamingWithNamespacedModelInIsolatedNamespaceTest < ActiveModel::TestCase def test_param_key assert_equal 'post', @model_name.param_key end + + def test_i18n_key + assert_equal :"blog/post", @model_name.i18n_key + end end class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase @@ -114,6 +122,10 @@ class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase def test_param_key assert_equal 'blog_post', @model_name.param_key end + + def test_i18n_key + assert_equal :"blog/post", @model_name.i18n_key + end end class NamingWithSuppliedModelNameTest < ActiveModel::TestCase @@ -154,6 +166,10 @@ class NamingWithSuppliedModelNameTest < ActiveModel::TestCase def test_param_key assert_equal 'article', @model_name.param_key end + + def test_i18n_key + assert_equal :"article", @model_name.i18n_key + end end class NamingUsingRelativeModelNameTest < ActiveModel::TestCase @@ -188,6 +204,10 @@ class NamingUsingRelativeModelNameTest < ActiveModel::TestCase def test_param_key assert_equal 'post', @model_name.param_key end + + def test_i18n_key + assert_equal :"blog/post", @model_name.i18n_key + end end class NamingHelpersTest < Test::Unit::TestCase @@ -197,6 +217,7 @@ class NamingHelpersTest < Test::Unit::TestCase @singular = 'contact' @plural = 'contacts' @uncountable = Sheep + @singular_route_key = 'contact' @route_key = 'contacts' @param_key = 'contact' end @@ -223,10 +244,12 @@ class NamingHelpersTest < Test::Unit::TestCase def test_route_key assert_equal @route_key, route_key(@record) + assert_equal @singular_route_key, singular_route_key(@record) end def test_route_key_for_class assert_equal @route_key, route_key(@klass) + assert_equal @singular_route_key, singular_route_key(@klass) end def test_param_key @@ -242,6 +265,11 @@ class NamingHelpersTest < Test::Unit::TestCase assert !uncountable?(@klass), "Expected 'contact' to be countable" end + def test_uncountable_route_key + assert_equal "sheep", singular_route_key(@uncountable) + assert_equal "sheep_index", route_key(@uncountable) + end + private def method_missing(method, *args) ActiveModel::Naming.send(method, *args) diff --git a/activemodel/test/cases/serializable_test.rb b/activemodel/test/cases/serialization_test.rb index 46ee372c6f..b8dad9d51f 100644 --- a/activemodel/test/cases/serializable_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/object/instance_variables' class SerializationTest < ActiveModel::TestCase class User - include ActiveModel::Serializable + include ActiveModel::Serialization attr_accessor :name, :email, :gender, :address, :friends @@ -22,7 +22,7 @@ class SerializationTest < ActiveModel::TestCase end class Address - include ActiveModel::Serializable + include ActiveModel::Serialization attr_accessor :street, :city, :state, :zip diff --git a/activemodel/test/cases/serializable/json_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index ad5b04091e..4ac5fb1779 100644 --- a/activemodel/test/cases/serializable/json_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -5,7 +5,7 @@ require 'active_support/core_ext/object/instance_variables' class Contact extend ActiveModel::Naming - include ActiveModel::Serializable::JSON + include ActiveModel::Serializers::JSON include ActiveModel::Validations def attributes=(hash) @@ -14,9 +14,11 @@ class Contact end end + remove_method :attributes if method_defined?(:attributes) + def attributes instance_values - end unless method_defined?(:attributes) + end end class JsonSerializationTest < ActiveModel::TestCase diff --git a/activemodel/test/cases/serializable/xml_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 817ca1e736..38aecf51ff 100644 --- a/activemodel/test/cases/serializable/xml_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -5,10 +5,12 @@ require 'ostruct' class Contact extend ActiveModel::Naming - include ActiveModel::Serializable::XML + include ActiveModel::Serializers::Xml attr_accessor :address, :friends + remove_method :attributes if method_defined?(:attributes) + def attributes instance_values.except("address", "friends") end @@ -24,7 +26,7 @@ end class Address extend ActiveModel::Naming - include ActiveModel::Serializable::XML + include ActiveModel::Serializers::Xml attr_accessor :street, :city, :state, :zip diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index 1b1d972d5c..54e86d48db 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -56,6 +56,16 @@ class ActiveModelI18nTests < ActiveModel::TestCase assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute') end + def test_translated_nested_model_attributes + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/addresses" => {:street => 'Person Address Street'}}} + assert_equal 'Person Address Street', Person.human_attribute_name('addresses.street') + end + + def test_translated_nested_model_attributes_with_namespace_fallback + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:addresses => {:street => 'Cool Address Street'}}} + assert_equal 'Cool Address Street', Person.human_attribute_name('addresses.street') + end + def test_translated_model_names I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} } assert_equal 'person model', Person.model_name.human diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index c50229e779..cd8e50b33a 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,87 @@ ## Rails 3.2.0 (unreleased) ## +* Added ability to run migrations only for given scope, which allows + to run migrations only from one engine (for example to revert changes + from engine that you want to remove). + + Example: + rake db:migrate SCOPE=blog + + *Piotr Sarnacki* + +* Migrations copied from engines are now scoped with engine's name, + for example 01_create_posts.blog.rb. *Piotr Sarnacki* + +* Implements `AR::Base.silence_auto_explain`. This method allows the user to + selectively disable automatic EXPLAINs within a block. *fxn* + +* Implements automatic EXPLAIN logging for slow queries. + + A new configuration parameter `config.active_record.auto_explain_threshold_in_seconds` + determines what's to be considered a slow query. Setting that to `nil` disables + this feature. Defaults are 0.5 in development mode, and `nil` in test and production + modes. + + As of this writing there's support for SQLite, MySQL (mysql2 adapter), and + PostgreSQL. + + *fxn* + +* Implemented ActiveRecord::Relation#pluck method + + Method returns Array of column value from table under ActiveRecord model + + Client.pluck(:id) + + *Bogdan Gusiev* + +* Automatic closure of connections in threads is deprecated. For example + the following code is deprecated: + + Thread.new { Post.find(1) }.join + + It should be changed to close the database connection at the end of + the thread: + + Thread.new { + Post.find(1) + Post.connection.close + }.join + + Only people who spawn threads in their application code need to worry + about this change. + +* Deprecated: + + * `set_table_name` + * `set_inheritance_column` + * `set_sequence_name` + * `set_primary_key` + * `set_locking_column` + + Use an assignment method instead. For example, instead of `set_table_name`, use `self.table_name=`: + + class Project < ActiveRecord::Base + self.table_name = "project" + end + + Or define your own `self.table_name` method: + + class Post < ActiveRecord::Base + def self.table_name + "special_" + super + end + end + Post.table_name # => "special_posts" + + *Jon Leighton* + +* Generated association methods are created within a separate module to allow overriding and + composition using `super`. For a class named `MyModel`, the module is named + `MyModel::GeneratedFeatureMethods`. It is included into the model class immediately after + the `generated_attributes_methods` module defined in ActiveModel, so association methods + override attribute methods of the same name. *Josh Susser* + * Implemented ActiveRecord::Relation#explain. *fxn* * Add ActiveRecord::Relation#uniq for generating unique queries. @@ -64,6 +146,10 @@ during :reject_if => :all_blank (fixes #2937) *Aaron Christy* + +* Add ActiveSupport::Cache::NullStore for use in development and testing. + + *Brian Durand* ## Rails 3.1.3 (unreleased) ## diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 52a3d4a501..b6a8db1ac0 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -21,6 +21,6 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('arel', '~> 3.0.0.pre') + s.add_dependency('arel', '~> 3.0.0.rc1') s.add_dependency('tzinfo', '~> 0.3.29') end diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index 63822731d5..31f3e02bb8 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -1,6 +1,6 @@ TIMES = (ENV['N'] || 10000).to_i -require 'rubygems' +require File.expand_path('../../../load_paths', __FILE__) require "active_record" conn = { :adapter => 'sqlite3', :database => ':memory:' } @@ -29,6 +29,14 @@ class Exhibit < ActiveRecord::Base def look; attributes end def feel; look; user.name end + def self.with_name + where("name IS NOT NULL") + end + + def self.with_notes + where("notes IS NOT NULL") + end + def self.look(exhibits) exhibits.each { |e| e.look } end def self.feel(exhibits) exhibits.each { |e| e.feel } end end @@ -37,7 +45,14 @@ puts 'Generating data...' module ActiveRecord class Faker - LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse non aliquet diam. Curabitur vel urna metus, quis malesuada elit. Integer consequat tincidunt felis. Etiam non erat dolor. Vivamus imperdiet nibh sit amet diam eleifend id posuere diam malesuada. Mauris at accumsan sem. Donec id lorem neque. Fusce erat lorem, ornare eu congue vitae, malesuada quis neque. Maecenas vel urna a velit pretium fermentum. Donec tortor enim, tempor venenatis egestas a, tempor sed ipsum. Ut arcu justo, faucibus non imperdiet ac, interdum at diam. Pellentesque ipsum enim, venenatis ut iaculis vitae, varius vitae sem. Sed rutrum quam ac elit euismod bibendum. Donec ultricies ultricies magna, at lacinia libero mollis aliquam. Sed ac arcu in tortor elementum tincidunt vel interdum sem. Curabitur eget erat arcu. Praesent eget eros leo. Nam magna enim, sollicitudin vehicula scelerisque in, vulputate ut libero. Praesent varius tincidunt commodo".split + LOREM = %Q{Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse non aliquet diam. Curabitur vel urna metus, quis malesuada elit. + Integer consequat tincidunt felis. Etiam non erat dolor. Vivamus imperdiet nibh sit amet diam eleifend id posuere diam malesuada. Mauris at accumsan sem. + Donec id lorem neque. Fusce erat lorem, ornare eu congue vitae, malesuada quis neque. Maecenas vel urna a velit pretium fermentum. Donec tortor enim, + tempor venenatis egestas a, tempor sed ipsum. Ut arcu justo, faucibus non imperdiet ac, interdum at diam. Pellentesque ipsum enim, venenatis ut iaculis vitae, + varius vitae sem. Sed rutrum quam ac elit euismod bibendum. Donec ultricies ultricies magna, at lacinia libero mollis aliquam. Sed ac arcu in tortor elementum + tincidunt vel interdum sem. Curabitur eget erat arcu. Praesent eget eros leo. Nam magna enim, sollicitudin vehicula scelerisque in, vulputate ut libero. + Praesent varius tincidunt commodo}.split + def self.name LOREM.grep(/^\w*$/).sort_by { rand }.first(2).join ' ' end @@ -102,6 +117,10 @@ Benchmark.bm(46) do |x| TIMES.times { Exhibit.first.look } end + x.report 'Model.named_scope' do + TIMES.times { Exhibit.limit(10).with_name.with_notes } + end + x.report("Model.all limit(100) (x#{(TIMES / 10).ceil})") do (TIMES / 10).ceil.times { Exhibit.look Exhibit.limit(100) } end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 3572c640eb..2463f3951f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -39,6 +39,7 @@ module ActiveRecord autoload :Aggregations autoload :Associations autoload :AttributeMethods + autoload :AttributeAssignment autoload :AutosaveAssociation autoload :Relation @@ -50,31 +51,42 @@ module ActiveRecord autoload :PredicateBuilder autoload :SpawnMethods autoload :Batches + autoload :Explain + autoload :Delegation end autoload :Base autoload :Callbacks autoload :CounterCache + autoload :DynamicMatchers autoload :DynamicFinderMatch autoload :DynamicScopeMatch + autoload :Explain + autoload :IdentityMap + autoload :Inheritance + autoload :Integration autoload :Migration autoload :Migrator, 'active_record/migration' - autoload :NamedScope + autoload :ModelSchema autoload :NestedAttributes autoload :Observer autoload :Persistence autoload :QueryCache + autoload :Querying + autoload :ReadonlyAttributes autoload :Reflection autoload :Result + autoload :Sanitization autoload :Schema autoload :SchemaDumper + autoload :Scoping autoload :Serialization - autoload :Store autoload :SessionStore + autoload :Store autoload :Timestamp autoload :Transactions + autoload :Translation autoload :Validations - autoload :IdentityMap end module Coders @@ -92,6 +104,8 @@ module ActiveRecord autoload :Read autoload :TimeZoneConversion autoload :Write + autoload :Serialization + autoload :DeprecatedUnderscoreRead end end @@ -113,6 +127,15 @@ module ActiveRecord end end + module Scoping + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Named + autoload :Default + end + end + autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 34684ad2f5..0efa111d12 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -70,7 +70,7 @@ module ActiveRecord end end - class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc + class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") end @@ -196,6 +196,26 @@ module ActiveRecord # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> # <tt>Project#categories.delete(category1)</tt> # + # === Overriding generated methods + # + # Association methods are generated in a module that is included into the model class, + # which allows you to easily override with your own methods and call the original + # generated method with +super+. For example: + # + # class Car < ActiveRecord::Base + # belongs_to :owner + # belongs_to :old_owner + # def owner=(new_owner) + # self.old_owner = self.owner + # super + # end + # end + # + # If your model class is <tt>Project</tt>, the module is + # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is + # included in the model class immediately after the (anonymous) generated attributes methods + # module, meaning an association will override the methods for an attribute with the same name. + # # === A word of warning # # Don't create associations that have the same name as instance methods of @@ -1165,7 +1185,7 @@ module ActiveRecord # has_many :subscribers, :through => :subscriptions, :source => :user # has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new { # %Q{ - # SELECT DISTINCT people.* + # SELECT DISTINCT * # FROM people p, post_subscriptions ps # WHERE ps.post_id = #{id} AND ps.person_id = p.id # ORDER BY p.first_name diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index d1e3ff8e38..861dda618a 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -230,6 +230,8 @@ module ActiveRecord end def build_record(attributes, options) + attributes = (attributes || {}).reverse_merge(creation_attributes) + reflection.build_association(attributes, options) do |record| record.assign_attributes( create_scope.except(*record.changed), diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 96fca97440..d4f59100e8 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -16,6 +16,10 @@ module ActiveRecord::Associations::Builder @model, @name, @options = model, name, options end + def mixin + @model.generated_feature_methods + end + def build validate_options reflection = model.create_reflection(self.class.macro, name, options, model) @@ -36,16 +40,14 @@ module ActiveRecord::Associations::Builder def define_readers name = self.name - - model.redefine_method(name) do |*params| + mixin.redefine_method(name) do |*params| association(name).reader(*params) end end def define_writers name = self.name - - model.redefine_method("#{name}=") do |value| + mixin.redefine_method("#{name}=") do |value| association(name).writer(value) end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index f6d26840c2..1759a41d93 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -25,14 +25,14 @@ module ActiveRecord::Associations::Builder name = self.name method_name = "belongs_to_counter_cache_after_create_for_#{name}" - model.redefine_method(method_name) do + mixin.redefine_method(method_name) do record = send(name) record.class.increment_counter(cache_column, record.id) unless record.nil? end model.after_create(method_name) method_name = "belongs_to_counter_cache_before_destroy_for_#{name}" - model.redefine_method(method_name) do + mixin.redefine_method(method_name) do record = send(name) record.class.decrement_counter(cache_column, record.id) unless record.nil? end @@ -48,7 +48,7 @@ module ActiveRecord::Associations::Builder method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}" touch = options[:touch] - model.redefine_method(method_name) do + mixin.redefine_method(method_name) do record = send(name) unless record.nil? diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index f62209a226..35f9a3ae8e 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -58,7 +58,7 @@ module ActiveRecord::Associations::Builder super name = self.name - model.redefine_method("#{name.to_s.singularize}_ids") do + mixin.redefine_method("#{name.to_s.singularize}_ids") do association(name).ids_reader end end @@ -67,7 +67,7 @@ module ActiveRecord::Associations::Builder super name = self.name - model.redefine_method("#{name.to_s.singularize}_ids=") do |ids| + mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids| association(name).ids_writer(ids) end end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 30fc44b4c2..0b634ab944 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -18,7 +18,7 @@ module ActiveRecord::Associations::Builder model.send(:include, Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 def destroy_associations - association(#{name.to_sym.inspect}).delete_all + association(#{name.to_sym.inspect}).delete_all_on_destroy super end RUBY diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index ecbc70888f..9c24f40690 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -28,7 +28,7 @@ module ActiveRecord::Associations::Builder def define_destroy_dependency_method name = self.name - model.send(:define_method, dependency_method_name) do + mixin.redefine_method(dependency_method_name) do send(name).each do |o| # No point in executing the counter update since we're going to destroy the parent anyway counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym @@ -45,15 +45,21 @@ module ActiveRecord::Associations::Builder def define_delete_all_dependency_method name = self.name - model.send(:define_method, dependency_method_name) do + mixin.redefine_method(dependency_method_name) do + association(name).delete_all_on_destroy + end + end + + def define_nullify_dependency_method + name = self.name + mixin.redefine_method(dependency_method_name) do send(name).delete_all end end - alias :define_nullify_dependency_method :define_delete_all_dependency_method def define_restrict_dependency_method name = self.name - model.send(:define_method, dependency_method_name) do + mixin.redefine_method(dependency_method_name) do raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty? end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 88c0d3e90f..7a6cd3890f 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -44,18 +44,17 @@ module ActiveRecord::Associations::Builder end def define_destroy_dependency_method - model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) - def #{dependency_method_name} - association(#{name.to_sym.inspect}).delete - end - eoruby + name = self.name + mixin.redefine_method(dependency_method_name) do + association(name).delete + end end alias :define_delete_dependency_method :define_destroy_dependency_method alias :define_nullify_dependency_method :define_destroy_dependency_method def define_restrict_dependency_method name = self.name - model.redefine_method(dependency_method_name) do + mixin.redefine_method(dependency_method_name) do raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil? end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 0cbbba041a..436b6c1524 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -16,15 +16,15 @@ module ActiveRecord::Associations::Builder def define_constructors name = self.name - model.redefine_method("build_#{name}") do |*params, &block| + mixin.redefine_method("build_#{name}") do |*params, &block| association(name).build(*params, &block) end - model.redefine_method("create_#{name}") do |*params, &block| + mixin.redefine_method("create_#{name}") do |*params, &block| association(name).create(*params, &block) end - model.redefine_method("create_#{name}!") do |*params, &block| + mixin.redefine_method("create_#{name}!") do |*params, &block| association(name).create!(*params, &block) end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index af37909c89..207080973c 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -152,6 +152,13 @@ module ActiveRecord end end + # Called when the association is declared as :dependent => :delete_all. This is + # an optimised version which avoids loading the records into memory. Not really + # for public consumption. + def delete_all_on_destroy + scoped.delete_all + end + # Destroy all the records from this association. # # See destroy for more info. diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 3181ca9a32..eb320bc774 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -39,10 +39,9 @@ module ActiveRecord instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, - :lock, :readonly, :having, :to => :scoped + :lock, :readonly, :having, :pluck, :to => :scoped - delegate :target, :load_target, :loaded?, :scoped, - :to => :@association + delegate :target, :load_target, :loaded?, :to => :@association delegate :select, :find, :first, :last, :build, :create, :create!, @@ -62,6 +61,13 @@ module ActiveRecord @association end + def scoped + association = @association + association.scoped.extending do + define_method(:proxy_association) { association } + end + end + def respond_to?(name, include_private = false) super || (load_target && target.respond_to?(name, include_private)) || 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 1f917f58f2..a4cea99372 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 @@ -32,6 +32,10 @@ module ActiveRecord record end + # ActiveRecord::Relation#delete_all needs to support joins before we can use a + # SQL-only implementation. + alias delete_all_on_destroy delete_all + private def count_records diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index c5b90e873a..059e6c77bc 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -89,12 +89,8 @@ module ActiveRecord records.each { |r| r.destroy } update_counter(-records.length) unless inverse_updates_counter_cache? else - scope = scoped - - unless records == load_target - keys = records.map { |r| r[reflection.association_primary_key] } - scope = scoped.where(reflection.association_primary_key => keys) - end + keys = records.map { |r| r[reflection.association_primary_key] } + scope = scoped.where(reflection.association_primary_key => keys) if method == :delete_all update_counter(-scope.delete_all) @@ -103,7 +99,7 @@ module ActiveRecord end end end - + def foreign_key_present? owner.attribute_present?(reflection.association_primary_key) 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 7e6e3be382..9657cb081d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -54,6 +54,10 @@ module ActiveRecord record end + # ActiveRecord::Relation#delete_all needs to support joins before we can use a + # SQL-only implementation. + alias delete_all_on_destroy delete_all + private def through_association diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 6c878f0f00..827b01c5ac 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -184,7 +184,7 @@ module ActiveRecord macro = join_part.reflection.macro if macro == :has_one - return if record.association_cache.key?(join_part.reflection.name) + return record.association(join_part.reflection.name).target if record.association_cache.key?(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 diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb new file mode 100644 index 0000000000..bf9fe70b31 --- /dev/null +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -0,0 +1,221 @@ +require 'active_support/concern' + +module ActiveRecord + module AttributeAssignment + extend ActiveSupport::Concern + include ActiveModel::MassAssignmentSecurity + + module ClassMethods + private + + # The primary key and inheritance column can never be set by mass-assignment for security reasons. + def attributes_protected_by_default + default = [ primary_key, inheritance_column ] + default << 'id' unless primary_key.eql? 'id' + default + end + end + + # Allows you to set all the attributes at once by passing in a hash with keys + # matching the attribute names (which again matches the column names). + # + # If any attributes are protected by either +attr_protected+ or + # +attr_accessible+ then only settable attributes will be assigned. + # + # class User < ActiveRecord::Base + # attr_protected :is_admin + # end + # + # user = User.new + # user.attributes = { :username => 'Phusion', :is_admin => true } + # user.username # => "Phusion" + # user.is_admin? # => false + def attributes=(new_attributes) + return unless new_attributes.is_a?(Hash) + + assign_attributes(new_attributes) + end + + # Allows you to set all the attributes for a particular mass-assignment + # security role by passing in a hash of attributes with keys matching + # the attribute names (which again matches the column names) and the role + # name using the :as option. + # + # To bypass mass-assignment security you can use the :without_protection => true + # option. + # + # class User < ActiveRecord::Base + # attr_accessible :name + # attr_accessible :name, :is_admin, :as => :admin + # end + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }) + # user.name # => "Josh" + # user.is_admin? # => false + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) + # user.name # => "Josh" + # user.is_admin? # => true + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) + # user.name # => "Josh" + # user.is_admin? # => true + def assign_attributes(new_attributes, options = {}) + return unless new_attributes + + attributes = new_attributes.stringify_keys + multi_parameter_attributes = [] + nested_parameter_attributes = [] + @mass_assignment_options = options + + unless options[:without_protection] + attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) + end + + attributes.each do |k, v| + if k.include?("(") + multi_parameter_attributes << [ k, v ] + elsif respond_to?("#{k}=") + if v.is_a?(Hash) + nested_parameter_attributes << [ k, v ] + else + send("#{k}=", v) + end + else + raise(UnknownAttributeError, "unknown attribute: #{k}") + end + end + + # assign any deferred nested attributes after the base attributes have been set + nested_parameter_attributes.each do |k,v| + send("#{k}=", v) + end + + @mass_assignment_options = nil + assign_multiparameter_attributes(multi_parameter_attributes) + end + + protected + + def mass_assignment_options + @mass_assignment_options ||= {} + end + + def mass_assignment_role + mass_assignment_options[:as] || :default + end + + private + + # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done + # by calling new on the column type or aggregation type (through composed_of) object with these parameters. + # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate + # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the + # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, + # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the + # attribute will be set to nil. + def assign_multiparameter_attributes(pairs) + execute_callstack_for_multiparameter_attributes( + extract_callstack_for_multiparameter_attributes(pairs) + ) + end + + def instantiate_time_object(name, values) + if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) + Time.zone.local(*values) + else + Time.time_with_datetime_fallback(self.class.default_timezone, *values) + end + end + + def execute_callstack_for_multiparameter_attributes(callstack) + errors = [] + callstack.each do |name, values_with_empty_parameters| + begin + send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) + rescue => ex + errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name) + end + end + unless errors.empty? + raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" + end + end + + def read_value_from_parameter(name, values_hash_from_param) + klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass + if values_hash_from_param.values.all?{|v|v.nil?} + nil + elsif klass == Time + read_time_parameter_value(name, values_hash_from_param) + elsif klass == Date + read_date_parameter_value(name, values_hash_from_param) + else + read_other_parameter_value(klass, name, values_hash_from_param) + end + end + + def read_time_parameter_value(name, values_hash_from_param) + # If Date bits were not provided, error + raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)} + max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) + # If Date bits were provided but blank, then return nil + return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} + + set_values = (1..max_position).collect{|position| values_hash_from_param[position] } + # If Time bits are not there, then default to 0 + (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]} + instantiate_time_object(name, set_values) + end + + def read_date_parameter_value(name, values_hash_from_param) + return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} + set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] + begin + Date.new(*set_values) + rescue ArgumentError # if Date.new raises an exception on an invalid date + instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates + end + end + + def read_other_parameter_value(klass, name, values_hash_from_param) + max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) + values = (1..max_position).collect do |position| + raise "Missing Parameter" if !values_hash_from_param.has_key?(position) + values_hash_from_param[position] + end + klass.new(*values) + end + + def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) + [values_hash_from_param.keys.max,upper_cap].min + end + + def extract_callstack_for_multiparameter_attributes(pairs) + attributes = { } + + pairs.each do |pair| + multiparameter_name, value = pair + attribute_name = multiparameter_name.split("(").first + attributes[attribute_name] = {} unless attributes.include?(attribute_name) + + parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) + attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value + end + + attributes + end + + def type_cast_attribute_value(multiparameter_name, value) + multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value + end + + def find_parameter_position(multiparameter_name) + multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i + end + + end +end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index d7bfaa5655..650fa3fc42 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -7,6 +7,29 @@ module ActiveRecord extend ActiveSupport::Concern include ActiveModel::AttributeMethods + included do + include Read + include Write + include BeforeTypeCast + include Query + include PrimaryKey + include TimeZoneConversion + include Dirty + include Serialization + include DeprecatedUnderscoreRead + + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, + # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). + # (Alias for the protected read_attribute method). + alias [] read_attribute + + # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. + # (Alias for the protected write_attribute method). + alias []= write_attribute + + public :[], :[]= + end + module ClassMethods # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. @@ -29,12 +52,30 @@ module ActiveRecord end end - def undefine_attribute_methods(*args) + def generated_attribute_methods + @generated_attribute_methods ||= (base_class == self ? super : base_class.generated_attribute_methods) + end + + def generated_external_attribute_methods + @generated_external_attribute_methods ||= begin + if base_class == self + # We will define the methods as instance methods, but will call them as singleton + # methods. This allows us to use method_defined? to check if the method exists, + # which is fast and won't give any false positives from the ancestors (because + # there are no ancestors). + Module.new { extend self } + else + base_class.generated_external_attribute_methods + end + end + end + + def undefine_attribute_methods if base_class == self super @attribute_methods_generated = false else - base_class.undefine_attribute_methods(*args) + base_class.undefine_attribute_methods end end @@ -57,6 +98,21 @@ module ActiveRecord !superclass.method_defined?(method_name) && !superclass.private_method_defined?(method_name) end + + def attribute_method?(attribute) + super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) + end + + # Returns an array of column names as strings if it's not + # an abstract class and table exists. + # Otherwise it returns an empty array. + def attribute_names + @attribute_names ||= if !abstract_class? && table_exists? + column_names + else + [] + end + end end # If we haven't generated any methods yet, generate them, then @@ -94,9 +150,105 @@ module ActiveRecord super end + # Returns true if the given attribute is in the attributes hash + def has_attribute?(attr_name) + @attributes.has_key?(attr_name.to_s) + end + + # Returns an array of names for the attributes available on this object. + def attribute_names + @attributes.keys + end + + # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. + def attributes + Hash[@attributes.map { |name, _| [name, read_attribute(name)] }] + end + + # Returns an <tt>#inspect</tt>-like string for the value of the + # attribute +attr_name+. String attributes are truncated upto 50 + # characters, and Date and Time attributes are returned in the + # <tt>:db</tt> format. Other attributes return the value of + # <tt>#inspect</tt> without modification. + # + # person = Person.create!(:name => "David Heinemeier Hansson " * 3) + # + # person.attribute_for_inspect(:name) + # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."' + # + # person.attribute_for_inspect(:created_at) + # # => '"2009-01-12 04:48:57"' + def attribute_for_inspect(attr_name) + value = read_attribute(attr_name) + + if value.is_a?(String) && value.length > 50 + "#{value[0..50]}...".inspect + elsif value.is_a?(Date) || value.is_a?(Time) + %("#{value.to_s(:db)}") + else + value.inspect + end + end + + # 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.nil? || (value.respond_to?(:empty?) && !value.empty?) + end + + # Returns the column object for the named attribute. + def column_for_attribute(name) + self.class.columns_hash[name.to_s] + end + protected - def attribute_method?(attr_name) - attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name)) + + def clone_attributes(reader_method = :read_attribute, attributes = {}) + attribute_names.each do |name| + attributes[name] = clone_attribute_value(reader_method, name) + end + attributes + end + + def clone_attribute_value(reader_method, attribute_name) + value = send(reader_method, attribute_name) + value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + value + end + + # Returns a copy of the attributes hash where all the values have been safely quoted for use in + # an Arel insert/update method. + def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) + attrs = {} + klass = self.class + arel_table = klass.arel_table + + attribute_names.each do |name| + if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) + + if include_readonly_attributes || !self.class.readonly_attributes.include?(name) + + value = if klass.serialized_attributes.include?(name) + @attributes[name].serialized_value + else + # FIXME: we need @attributes to be used consistently. + # If the values stored in @attributes were already type + # casted, this code could be simplified + read_attribute(name) + end + + attrs[arel_table[name]] = value + end + end end + + attrs + end + + def attribute_method?(attr_name) + attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name)) + end end end diff --git a/activerecord/lib/active_record/attribute_methods/deprecated_underscore_read.rb b/activerecord/lib/active_record/attribute_methods/deprecated_underscore_read.rb new file mode 100644 index 0000000000..0eb0db65b1 --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/deprecated_underscore_read.rb @@ -0,0 +1,32 @@ +require 'active_support/concern' +require 'active_support/deprecation' + +module ActiveRecord + module AttributeMethods + module DeprecatedUnderscoreRead + extend ActiveSupport::Concern + + included do + attribute_method_prefix "_" + end + + module ClassMethods + protected + + def define_method__attribute(attr_name) + # Do nothing, let it hit method missing instead. + end + end + + protected + + def _attribute(attr_name) + ActiveSupport::Deprecation.warn( + "You have called '_#{attr_name}'. This is deprecated. Please use " \ + "either '#{attr_name}' or read_attribute('#{attr_name}')." + ) + read_attribute(attr_name) + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 3eff3d54e3..f61e016f46 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -34,9 +34,9 @@ module ActiveRecord @previously_changed = changes @changed_attributes.clear end - rescue - IdentityMap.remove(self) if IdentityMap.enabled? - raise + rescue + IdentityMap.remove(self) if IdentityMap.enabled? + raise end # <tt>reload</tt> the record and clears changed attributes. diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index a404a5edd7..5d37088d98 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -5,15 +5,49 @@ module ActiveRecord # Returns this record's primary key value wrapped in an Array if one is available def to_key - key = send(self.class.primary_key) + key = self.id [key] if key end + # Returns the primary key value + def id + read_attribute(self.class.primary_key) + end + + # Sets the primary key value + def id=(value) + write_attribute(self.class.primary_key, value) + end + + # Queries the primary key value + def id? + query_attribute(self.class.primary_key) + end + module ClassMethods + def define_method_attribute(attr_name) + super + + if attr_name == primary_key && attr_name != 'id' + generated_attribute_methods.send(:alias_method, :id, primary_key) + generated_external_attribute_methods.module_eval <<-CODE, __FILE__, __LINE__ + def id(v, attributes, attributes_cache, attr_name) + attr_name = '#{primary_key}' + send(attr_name, attributes[attr_name], attributes, attributes_cache, attr_name) + end + CODE + end + end + + def dangerous_attribute_method?(method_name) + super && !['id', 'id=', 'id?'].include?(method_name) + end + # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the # primary_key_prefix_type setting, though. def primary_key - @primary_key ||= reset_primary_key + @primary_key = reset_primary_key unless defined? @primary_key + @primary_key end # Returns a quoted version of the primary key name, used to construct SQL statements. @@ -22,11 +56,11 @@ module ActiveRecord end def reset_primary_key #:nodoc: - key = self == base_class ? get_primary_key(base_class.name) : - base_class.primary_key - - set_primary_key(key) - key + if self == base_class + self.primary_key = get_primary_key(base_class.name) + else + self.primary_key = base_class.primary_key + end end def get_primary_key(base_name) #:nodoc: @@ -38,35 +72,41 @@ module ActiveRecord when :table_name_with_underscore base_name.foreign_key else - if ActiveRecord::Base != self && connection.table_exists?(table_name) - connection.primary_key(table_name) + if ActiveRecord::Base != self && table_exists? + connection.schema_cache.primary_keys[table_name] else 'id' end end end - attr_accessor :original_primary_key - - # Attribute writer for the primary key column - def primary_key=(value) - @quoted_primary_key = nil - @primary_key = value + def original_primary_key #:nodoc: + deprecated_original_property_getter :primary_key end - # Sets the name of the primary key column to use to the given value, - # or (if the value is nil or false) to the value returned by the given - # block. + # Sets the name of the primary key column. + # + # class Project < ActiveRecord::Base + # self.primary_key = "sysid" + # end + # + # You can also define the primary_key method yourself: # # class Project < ActiveRecord::Base - # set_primary_key "sysid" + # def self.primary_key + # "foo_" + super + # end # end - def set_primary_key(value = nil, &block) + # Project.primary_key # => "foo_id" + def primary_key=(value) + @original_primary_key = @primary_key if defined?(@primary_key) + @primary_key = value && value.to_s + @quoted_primary_key = nil + end + + def set_primary_key(value = nil, &block) #:nodoc: + deprecated_property_setter :primary_key, value, block @quoted_primary_key = nil - @primary_key ||= '' - self.original_primary_key = @primary_key - value &&= value.to_s - self.primary_key = block_given? ? instance_eval(&block) : value end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 4a5afcd585..77bf9d0905 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -8,9 +8,6 @@ module ActiveRecord included do cattr_accessor :attribute_types_cached_by_default, :instance_writer => false self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - - # Undefine id so it can be used as an attribute name - undef_method(:id) if method_defined?(:id) end module ClassMethods @@ -32,109 +29,103 @@ module ActiveRecord cached_attributes.include?(attr_name) end + def undefine_attribute_methods + if base_class == self + generated_external_attribute_methods.module_eval do + instance_methods.each { |m| undef_method(m) } + end + end + + super + end + protected + # We want to generate the methods via module_eval rather than define_method, + # because define_method is slower on dispatch and uses more memory (because it + # creates a closure). + # + # But sometimes the database might return columns with characters that are not + # allowed in normal method names (like 'my_column(omg)'. So to work around this + # we first define with the __temp__ identifier, and then use alias method to + # rename it to what we want. def define_method_attribute(attr_name) - if serialized_attributes.include?(attr_name) - define_read_method_for_serialized_attribute(attr_name) - else - define_read_method(attr_name, attr_name, columns_hash[attr_name]) - end + cast_code = attribute_cast_code(attr_name) - if attr_name == primary_key && attr_name != "id" - define_read_method('id', attr_name, columns_hash[attr_name]) - end + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__ + #{internal_attribute_access_code(attr_name, cast_code)} + end + alias_method '#{attr_name}', :__temp__ + undef_method :__temp__ + STR + + generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__(v, attributes, attributes_cache, attr_name) + #{external_attribute_access_code(attr_name, cast_code)} + end + alias_method '#{attr_name}', :__temp__ + undef_method :__temp__ + STR end private def cacheable_column?(column) - serialized_attributes.include?(column.name) || attribute_types_cached_by_default.include?(column.type) - end - - # Define read method for serialized attribute. - def define_read_method_for_serialized_attribute(attr_name) - access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']" - generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__) + attribute_types_cached_by_default.include?(column.type) end - # Define an attribute reader method. Cope with nil column. - # method_name is the same as attr_name except when a non-standard primary key is used, - # we still define #id as an accessor for the key - def define_read_method(method_name, attr_name, column) - cast_code = column.type_cast_code('v') - access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}" + def internal_attribute_access_code(attr_name, cast_code) + access_code = "(v=@attributes[attr_name]) && #{cast_code}" - unless attr_name.to_s == self.primary_key.to_s - access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + unless attr_name == primary_key + access_code.insert(0, "missing_attribute(attr_name, caller) unless @attributes.has_key?(attr_name); ") end if cache_attribute?(attr_name) - access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" + access_code = "@attributes_cache[attr_name] ||= (#{access_code})" end - # Where possible, generate the method by evalling a string, as this will result in - # faster accesses because it avoids the block eval and then string eval incurred - # by the second branch. - # - # The second, slower, branch is necessary to support instances where the database - # returns columns with extra stuff in (like 'my_column(omg)'). - if method_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ - def _#{method_name} - #{access_code} - end - - alias #{method_name} _#{method_name} - STR - else - generated_attribute_methods.module_eval do - define_method("_#{method_name}") { eval(access_code) } - alias_method(method_name, "_#{method_name}") - end + "attr_name = '#{attr_name}'; #{access_code}" + end + + def external_attribute_access_code(attr_name, cast_code) + access_code = "v && #{cast_code}" + + if cache_attribute?(attr_name) + access_code = "attributes_cache[attr_name] ||= (#{access_code})" end + + access_code + end + + def attribute_cast_code(attr_name) + columns_hash[attr_name].type_cast_code('v') end end # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) - method = "_#{attr_name}" - if respond_to? method - send method if @attributes.has_key?(attr_name.to_s) - else - _read_attribute attr_name - end - end + return unless attr_name - def _read_attribute(attr_name) attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' - value = @attributes[attr_name] - unless value.nil? - if column = column_for_attribute(attr_name) - if unserializable_attribute?(attr_name, column) - unserialize_attribute(attr_name) - else - column.type_cast(value) - end - else - value + methods = self.class.generated_external_attribute_methods + + if methods.method_defined?(attr_name) + if @attributes.has_key?(attr_name) || attr_name == 'id' + methods.send(attr_name, @attributes[attr_name], @attributes, @attributes_cache, attr_name) end + elsif !self.class.attribute_methods_generated? + # If we haven't generated the caster methods yet, do that and + # then try again + self.class.define_attribute_methods + read_attribute(attr_name) + else + # If we get here, the attribute has no associated DB column, so + # just return it verbatim. + @attributes[attr_name] end end - # Returns true if the attribute is of a text column and marked for serialization. - def unserializable_attribute?(attr_name, column) - column.text? && self.class.serialized_attributes.include?(attr_name) - end - - # Returns the unserialized object of the attribute. - def unserialize_attribute(attr_name) - coder = self.class.serialized_attributes[attr_name] - unserialized_object = coder.load(@attributes[attr_name]) - - @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object - end - private def attribute(attribute_name) read_attribute(attribute_name) diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb new file mode 100644 index 0000000000..0a4432506f --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -0,0 +1,89 @@ +module ActiveRecord + module AttributeMethods + module Serialization + extend ActiveSupport::Concern + + included do + # Returns a hash of all the attributes that have been specified for serialization as + # keys and their class restriction as values. + class_attribute :serialized_attributes + self.serialized_attributes = {} + end + + class Attribute < Struct.new(:coder, :value, :state) + def unserialized_value + state == :serialized ? unserialize : value + end + + def serialized_value + state == :unserialized ? serialize : value + end + + def unserialize + self.state = :unserialized + self.value = coder.load(value) + end + + def serialize + self.state = :serialized + self.value = coder.dump(value) + end + end + + module ClassMethods + # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, + # then specify the name of that attribute using this method and it will be handled automatically. + # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that + # class on retrieval or SerializationTypeMismatch will be raised. + # + # ==== Parameters + # + # * +attr_name+ - The field name that should be serialized. + # * +class_name+ - Optional, class name that the object type should be equal to. + # + # ==== Example + # # Serialize a preferences attribute + # class User < ActiveRecord::Base + # serialize :preferences + # end + def serialize(attr_name, class_name = Object) + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end + + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) + end + + private + + def attribute_cast_code(attr_name) + if serialized_attributes.include?(attr_name) + "v.unserialized_value" + else + super + end + end + end + + def set_serialized_attributes + self.class.serialized_attributes.each do |key, coder| + if @attributes.key?(key) + @attributes[key] = Attribute.new(coder, @attributes[key], :serialized) + end + end + end + + def type_cast_attribute_for_write(column, value) + if column && coder = self.class.serialized_attributes[column.name] + Attribute.new(coder, value, :unserialized) + else + super + end + end + end + end +end 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 62a3cfa9a5..17cf34cdf6 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -16,21 +16,16 @@ module ActiveRecord module ClassMethods protected - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced read method automatically converts the UTC time stored in the database to the time + # The enhanced read method automatically converts the UTC time stored in the database to the time # zone stored in Time.zone. - def define_method_attribute(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body, line = <<-EOV, __LINE__ + 1 - def _#{attr_name} - cached = @attributes_cache['#{attr_name}'] - return cached if cached - time = _read_attribute('#{attr_name}') - @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time - end - alias #{attr_name} _#{attr_name} - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, line) + def attribute_cast_code(attr_name) + column = columns_hash[attr_name] + + if create_time_zone_conversion_attribute?(attr_name, column) + typecast = "v = #{super}" + time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v" + + "((#{typecast}) && (#{time_zone_conversion}))" else super end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index eb585ee906..fde55b95da 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -17,10 +17,6 @@ module ActiveRecord write_attribute(attr_name, new_value) end end - - if attr_name == primary_key && attr_name != "id" - generated_attribute_methods.module_eval("alias :id= :'#{primary_key}='") - end end end @@ -32,10 +28,8 @@ module ActiveRecord @attributes_cache.delete(attr_name) column = column_for_attribute(attr_name) - if column && column.number? - @attributes[attr_name] = convert_number_column_value(value) - elsif column || @attributes.has_key?(attr_name) - @attributes[attr_name] = value + if column || @attributes.has_key?(attr_name) + @attributes[attr_name] = type_cast_attribute_for_write(column, value) else raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" end @@ -47,6 +41,26 @@ module ActiveRecord def attribute=(attribute_name, value) write_attribute(attribute_name, value) end + + def type_cast_attribute_for_write(column, value) + if column && column.number? + convert_number_column_value(value) + else + value + end + end + + def convert_number_column_value(value) + if value == false + 0 + elsif value == true + 1 + elsif value.is_a?(String) && value.blank? + nil + else + value + end + end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 056170d82a..c86eaba498 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -21,6 +21,21 @@ module ActiveRecord # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>. # When the <tt>:autosave</tt> option is not present new associations are saved. # + # == Validation + # + # Children records are validated unless <tt>:validate</tt> is +false+. + # + # == Callbacks + # + # Association with autosave option defines several callbacks on your + # model (before_save, after_create, after_update). Please note that + # callbacks are executed in the order they were defined in + # model. You should avoid modyfing the association content, before + # autosave callbacks are executed. Placing your callbacks after + # associations is usually a good practice. + # + # == Examples + # # === One-to-one Example # # class Post @@ -109,10 +124,7 @@ module ActiveRecord # Now it _is_ removed from the database: # # Comment.find_by_id(id).nil? # => true - # - # === Validation - # - # Children records are validated unless <tt>:validate</tt> is +false+. + module AutosaveAssociation extend ActiveSupport::Concern @@ -264,7 +276,7 @@ module ActiveRecord # turned on for the association. def validate_single_association(reflection) association = association_instance_get(reflection.name) - record = association && association.target + record = association && association.reader association_valid?(reflection, record) if record end @@ -331,7 +343,7 @@ module ActiveRecord if autosave saved = association.insert_record(record, false) else - association.insert_record(record) + association.insert_record(record) unless reflection.nested? end elsif autosave saved = record.save(:validate => false) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 7ba67b8540..b479c403a7 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -23,9 +23,11 @@ require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/object/blank' +require 'active_support/deprecation' require 'arel' require 'active_record/errors' require 'active_record/log_subscriber' +require 'active_record/explain_subscriber' module ActiveRecord #:nodoc: # = Active Record @@ -364,44 +366,6 @@ module ActiveRecord #:nodoc: ## # :singleton-method: - # Accessor for the prefix type that will be prepended to every primary key column name. - # The options are :table_name and :table_name_with_underscore. If the first is specified, - # the Product class will look for "productid" instead of "id" as the primary column. If the - # latter is specified, the Product class will look for "product_id" instead of "id". Remember - # that this is a global setting for all Active Records. - cattr_accessor :primary_key_prefix_type, :instance_writer => false - @@primary_key_prefix_type = nil - - ## - # :singleton-method: - # Accessor for the name of the prefix string to prepend to every table name. So if set - # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people", - # etc. This is a convenient way of creating a namespace for tables in a shared database. - # By default, the prefix is the empty string. - # - # If you are organising your models within modules you can add a prefix to the models within - # a namespace by defining a singleton method in the parent module called table_name_prefix which - # returns your chosen prefix. - class_attribute :table_name_prefix, :instance_writer => false - self.table_name_prefix = "" - - ## - # :singleton-method: - # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", - # "people_basecamp"). By default, the suffix is the empty string. - class_attribute :table_name_suffix, :instance_writer => false - self.table_name_suffix = "" - - ## - # :singleton-method: - # Indicates whether table names should be the pluralized versions of the corresponding class names. - # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. - # See table_name for the full rules on table/class naming. This is true, by default. - class_attribute :pluralize_table_names, :instance_writer => false - self.pluralize_table_names = true - - ## - # :singleton-method: # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling # dates and times from the database. This is set to :local by default. cattr_accessor :default_timezone, :instance_writer => false @@ -424,419 +388,22 @@ module ActiveRecord #:nodoc: cattr_accessor :timestamped_migrations , :instance_writer => false @@timestamped_migrations = true - # Determine whether to store the full constant name including namespace when using STI - class_attribute :store_full_sti_class - self.store_full_sti_class = true - - # Stores the default scope for the class - class_attribute :default_scopes, :instance_writer => false - self.default_scopes = [] - - # Returns a hash of all the attributes that have been specified for serialization as - # keys and their class restriction as values. - class_attribute :serialized_attributes - self.serialized_attributes = {} - - class_attribute :_attr_readonly, :instance_writer => false - self._attr_readonly = [] - class << self # Class methods - delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped - delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped - delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped - delegate :find_each, :find_in_batches, :to => :scoped - delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, - :where, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :to => :scoped - delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped - - # Executes a custom SQL query against your database and returns all the results. The results will - # be returned as an array with columns requested encapsulated as attributes of the model you call - # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in - # a Product object with the attributes you specified in the SQL query. - # - # If you call a complicated SQL query which spans multiple tables the columns specified by the - # SELECT will be attributes of the model, whether or not they are columns of the corresponding - # table. - # - # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be - # no database agnostic conversions performed. This should be a last resort because using, for example, - # MySQL specific terms will lock you to using that particular database engine or require you to - # change your call if you switch engines. - # - # ==== Examples - # # A simple SQL query spanning multiple tables - # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id" - # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...] - # - # # 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={"title"=>"The Cheap Man Buys Twice"}>, ...] - 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. - # The resulting object is returned whether the object was saved successfully to the database or not. - # - # The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the - # attributes on the objects that are to be created. - # - # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options - # in the +options+ parameter. - # - # ==== Examples - # # Create a single new object - # User.create(:first_name => 'Jamie') - # - # # Create a single new object using the :admin mass-assignment security role - # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) - # - # # Create a single new object bypassing mass-assignment security - # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) - # - # # Create an Array of new objects - # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) - # - # # Create a single object and pass it into a block to set other attributes. - # User.create(:first_name => 'Jamie') do |u| - # u.is_admin = false - # end - # - # # Creating an Array of new objects using a block, where the block is executed for each object: - # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u| - # u.is_admin = false - # end - def create(attributes = nil, options = {}, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr, options, &block) } - else - object = new(attributes, options, &block) - object.save - object - end - end - - # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. - # The use of this method should be restricted to complicated SQL queries that can't be executed - # using the ActiveRecord::Calculations class methods. Look into those before using this. - # - # ==== Parameters - # - # * +sql+ - An SQL statement which should return a count query from the database, see the example below. - # - # ==== Examples - # - # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" - def count_by_sql(sql) - sql = sanitize_conditions(sql) - connection.select_value(sql, "#{name} Count").to_i - end - - # Attributes listed as readonly will be used to create a new record but update operations will - # ignore these fields. - def attr_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 - 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, - # then specify the name of that attribute using this method and it will be handled automatically. - # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that - # class on retrieval or SerializationTypeMismatch will be raised. - # - # ==== Parameters - # - # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. - # - # ==== Example - # # Serialize a preferences attribute - # class User < ActiveRecord::Base - # serialize :preferences - # end - def serialize(attr_name, class_name = Object) - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name - else - Coders::YAMLColumn.new(class_name) - end - - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) - end - - # Guesses the table name (in forced lower-case) based on the name of the class in the - # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy - # looks like: Reply < Message < ActiveRecord::Base, then Message is used - # to guess the table name even when called on Reply. The rules used to do the guess - # are handled by the Inflector class in Active Support, which knows almost all common - # English inflections. You can add new inflections in config/initializers/inflections.rb. - # - # Nested classes are given table names prefixed by the singular form of - # the parent's table name. Enclosing modules are not considered. - # - # ==== Examples - # - # class Invoice < ActiveRecord::Base - # end - # - # file class table_name - # invoice.rb Invoice invoices - # - # class Invoice < ActiveRecord::Base - # class Lineitem < ActiveRecord::Base - # end - # end - # - # file class table_name - # invoice.rb Invoice::Lineitem invoice_lineitems - # - # module Invoice - # class Lineitem < ActiveRecord::Base - # end - # end - # - # file class table_name - # invoice/lineitem.rb Invoice::Lineitem lineitems - # - # Additionally, the class-level +table_name_prefix+ is prepended and the - # +table_name_suffix+ is appended. So if you have "myapp_" as a prefix, - # the table name guess for an Invoice class becomes "myapp_invoices". - # Invoice::Lineitem becomes "myapp_invoice_lineitems". - # - # You can also overwrite this class method to allow for unguessable - # links, such as a Mouse class with a link to a "mice" table. Example: - # - # class Mouse < ActiveRecord::Base - # set_table_name "mice" - # end - def table_name - reset_table_name - end - - # Returns a quoted version of the table name, used to construct SQL statements. - def quoted_table_name - @quoted_table_name ||= connection.quote_table_name(table_name) - end - - # Computes the table name, (re)sets it internally, and returns it. - def reset_table_name #:nodoc: - return if abstract_class? - - self.table_name = compute_table_name - end - - def full_table_name_prefix #:nodoc: - (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix - end - - # Defines the column name for use with single table inheritance. Use - # <tt>set_inheritance_column</tt> to set a different value. - def inheritance_column - @inheritance_column ||= "type" - end - - # Lazy-set the sequence name to the connection's default. This method - # is only ever called once since set_sequence_name overrides it. - def sequence_name #:nodoc: - reset_sequence_name - end - - def reset_sequence_name #:nodoc: - default = connection.default_sequence_name(table_name, primary_key) - set_sequence_name(default) - default - end - - # Sets the table name. If the value is nil or false then the value returned by the given - # block is used. - # - # class Project < ActiveRecord::Base - # set_table_name "project" - # end - def set_table_name(value = nil, &block) - @quoted_table_name = nil - define_attr_method :table_name, value, &block - @arel_table = nil - - @relation = Relation.new(self, arel_table) - end - alias :table_name= :set_table_name - - # Sets the name of the inheritance column to use to the given value, - # or (if the value # is nil or false) to the value returned by the - # given block. - # - # class Project < ActiveRecord::Base - # set_inheritance_column do - # original_inheritance_column + "_id" - # end - # end - def set_inheritance_column(value = nil, &block) - define_attr_method :inheritance_column, value, &block - end - alias :inheritance_column= :set_inheritance_column - - # Sets the name of the sequence to use when generating ids to the given - # value, or (if the value is nil or false) to the value returned by the - # given block. This is required for Oracle and is useful for any - # database which relies on sequences for primary key generation. - # - # If a sequence name is not explicitly set when using Oracle or Firebird, - # it will default to the commonly used pattern of: #{table_name}_seq - # - # If a sequence name is not explicitly set when using PostgreSQL, it - # will discover the sequence corresponding to your primary key for you. - # - # class Project < ActiveRecord::Base - # set_sequence_name "projectseq" # default would have been "project_seq" - # end - def set_sequence_name(value = nil, &block) - define_attr_method :sequence_name, value, &block - end - alias :sequence_name= :set_sequence_name - - # Indicates whether the table associated with this class exists - def table_exists? - connection.table_exists?(table_name) - end - - # Returns an array of column objects for the table associated with this class. - def columns - if defined?(@primary_key) - connection.schema_cache.primary_keys[table_name] ||= primary_key - end - - connection.schema_cache.columns[table_name] - end - - # Returns a hash of column objects for the table associated with this class. - def columns_hash - connection.schema_cache.columns_hash[table_name] - end - - # Returns a hash where the keys are column names and the values are - # default values when instantiating the AR object for this table. - def column_defaults - connection.schema_cache.column_defaults[table_name] - end - - # Returns an array of column names as strings. - def column_names - @column_names ||= columns.map { |column| column.name } - end - - # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", - # and columns used for single table inheritance have been removed. - def content_columns - @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } - end - - # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key - # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute - # is available. - def column_methods_hash #:nodoc: - @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| - attr_name = attr.to_s - methods[attr.to_sym] = attr_name - methods["#{attr}=".to_sym] = attr_name - methods["#{attr}?".to_sym] = attr_name - methods["#{attr}_before_type_cast".to_sym] = attr_name - methods - end - end - - # Resets all the cached information about columns, which will cause them - # to be reloaded on the next request. - # - # The most common usage pattern for this method is probably in a migration, - # when just after creating a table you want to populate it with some default - # values, eg: - # - # class CreateJobLevels < ActiveRecord::Migration - # def up - # create_table :job_levels do |t| - # t.integer :id - # t.string :name - # - # t.timestamps - # end - # - # JobLevel.reset_column_information - # %w{assistant executive manager director}.each do |type| - # JobLevel.create(:name => type) - # end - # end - # - # def down - # drop_table :job_levels - # end - # end - def reset_column_information - connection.clear_cache! - undefine_attribute_methods - connection.schema_cache.clear_table_cache!(table_name) if table_exists? - - @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil - @arel_engine = @relation = nil - end - - def clear_cache! # :nodoc: - connection.schema_cache.clear! - end - - def attribute_method?(attribute) - super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) - end - - # Returns an array of column names as strings if it's not - # an abstract class and table exists. - # Otherwise it returns an empty array. - def attribute_names - @attribute_names ||= if !abstract_class? && table_exists? - column_names - else - [] - end - end - - # Set the lookup ancestors for ActiveModel. - 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 - end - - # Set the i18n scope to overwrite ActiveModel. - def i18n_scope #:nodoc: - :activerecord + def inherited(child_class) #:nodoc: + # force attribute methods to be higher in inheritance hierarchy than other generated methods + child_class.generated_attribute_methods + child_class.generated_feature_methods + super end - # True if this isn't a concrete subclass needing a STI type condition. - def descends_from_active_record? - if superclass.abstract_class? - superclass.descends_from_active_record? - else - superclass == Base || !columns_hash.include?(inheritance_column) + def generated_feature_methods + @generated_feature_methods ||= begin + mod = const_set(:GeneratedFeatureMethods, Module.new) + include mod + mod end end - def finder_needs_type_condition? #:nodoc: - # This is like this because benchmarking justifies the strange :false stuff - :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) - end - # Returns a string like 'Post(id:integer, title:string, body:text)' def inspect if self == Base @@ -851,60 +418,11 @@ module ActiveRecord #:nodoc: end end - def quote_value(value, column = nil) #:nodoc: - connection.quote(value,column) - end - - # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. - def sanitize(object) #:nodoc: - connection.quote(object) - end - # Overwrite the default class equality method to provide support for association proxies. def ===(object) object.is_a?(self) end - def symbolized_base_class - @symbolized_base_class ||= base_class.to_s.to_sym - end - - def symbolized_sti_name - @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class - end - - # Returns the base AR subclass that this class descends from. If A - # extends AR::Base, A.base_class will return A. If B descends from A - # through some arbitrarily deep hierarchy, B.base_class will return A. - # - # If B < A and C < B and if A is an abstract_class then both B.base_class - # and C.base_class would return B as the answer since A is an abstract_class. - def base_class - class_of_active_record_descendant(self) - end - - # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). - attr_accessor :abstract_class - - # Returns whether this class is an abstract class or not. - def abstract_class? - defined?(@abstract_class) && @abstract_class == true - end - - def respond_to?(method_id, include_private = false) - if match = DynamicFinderMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - elsif match = DynamicScopeMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - end - - super - end - - def sti_name - store_full_sti_class ? name : name.demodulize - end - def arel_table @arel_table ||= Arel::Table.new(table_name, arel_engine) end @@ -919,606 +437,17 @@ module ActiveRecord #:nodoc: end end - # Returns a scope for this class without taking into account the default_scope. - # - # class Post < ActiveRecord::Base - # def self.default_scope - # where :published => true - # end - # end - # - # Post.all # Fires "SELECT * FROM posts WHERE published = true" - # Post.unscoped.all # Fires "SELECT * FROM posts" - # - # This method also accepts a block meaning that all queries inside the block will - # not use the default_scope: - # - # Post.unscoped { - # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" - # } - # - # It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt> - # does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same. - # - # Post.unscoped.published - # Post.published - def unscoped #:nodoc: - block_given? ? relation.scoping { yield } : relation - end - - def before_remove_const #:nodoc: - self.current_scope = nil - end + private - # Finder methods must instantiate through this method to work with the - # single-table inheritance model that makes it possible to create - # objects of different types from the same table. - def instantiate(record) - sti_class = find_sti_class(record[inheritance_column]) - record_id = sti_class.primary_key && record[sti_class.primary_key] + def relation #:nodoc: + @relation ||= Relation.new(self, arel_table) - if ActiveRecord::IdentityMap.enabled? && record_id - if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? - record_id = record_id.to_i - end - if instance = IdentityMap.get(sti_class, record_id) - instance.reinit_with('attributes' => record) - else - instance = sti_class.allocate.init_with('attributes' => record) - IdentityMap.add(instance) - end + if finder_needs_type_condition? + @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) else - instance = sti_class.allocate.init_with('attributes' => record) + @relation end - - instance end - - private - - def relation #:nodoc: - @relation ||= Relation.new(self, arel_table) - - if finder_needs_type_condition? - @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) - else - @relation - end - end - - def find_sti_class(type_name) - if type_name.blank? || !columns_hash.include?(inheritance_column) - self - else - begin - if store_full_sti_class - ActiveSupport::Dependencies.constantize(type_name) - else - compute_type(type_name) - end - rescue NameError - raise SubclassNotFound, - "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + - "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + - "Please rename this column if you didn't intend it to be used for storing the inheritance class " + - "or overwrite #{name}.inheritance_column to use another column for that information." - end - end - end - - def construct_finder_arel(options = {}, scope = nil) - relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options - relation = scope.merge(relation) if scope - relation - end - - def type_condition(table = arel_table) - sti_column = table[inheritance_column.to_sym] - sti_names = ([self] + descendants).map { |model| model.sti_name } - - sti_column.in(sti_names) - end - - # Guesses the table name, but does not decorate it with prefix and suffix information. - def undecorated_table_name(class_name = base_class.name) - table_name = class_name.to_s.demodulize.underscore - table_name = table_name.pluralize if pluralize_table_names - table_name - end - - # Computes and returns a table name according to default conventions. - def compute_table_name - base = base_class - if self == base - # Nested classes are prefixed with singular parent table name. - if parent < ActiveRecord::Base && !parent.abstract_class? - contained = parent.table_name - contained = contained.singularize if parent.pluralize_table_names - contained += '_' - end - "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" - else - # STI subclasses always use their superclass' table. - base.table_name - end - end - - # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and - # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders - # section at the top of this file for more detailed information. - # - # It's even possible to use all the additional parameters to +find+. For example, the - # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. - # - # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it - # is first invoked, so that future attempts to use it do not run through method_missing. - def method_missing(method_id, *arguments, &block) - if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)) - attribute_names = match.attribute_names - super unless all_attributes_exists?(attribute_names) - if arguments.size < attribute_names.size - method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" - backtrace = [method_trace] + caller - raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace - end - if match.respond_to?(:scope?) && match.scope? - self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) - attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)] - # - scoped(:conditions => attributes) # scoped(:conditions => attributes) - end # end - METHOD - send(method_id, *arguments) - elsif match.finder? - options = arguments.extract_options! - relation = options.any? ? scoped(options) : scoped - relation.send :find_by_attributes, match, attribute_names, *arguments, &block - elsif match.instantiator? - scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block - end - else - super - end - end - - # Similar in purpose to +expand_hash_conditions_for_aggregates+. - def expand_attribute_names_for_aggregates(attribute_names) - attribute_names.map { |attribute_name| - unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? - aggregate_mapping(aggregation).map do |field_attr, _| - field_attr.to_sym - end - else - attribute_name.to_sym - end - }.flatten - end - - def all_attributes_exists?(attribute_names) - (expand_attribute_names_for_aggregates(attribute_names) - - column_methods_hash.keys).empty? - end - - protected - # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be - # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while - # <tt>:create</tt> parameters are an attributes hash. - # - # class Article < ActiveRecord::Base - # def self.create_with_scope - # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do - # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 - # a = create(1) - # a.blog_id # => 1 - # end - # end - # end - # - # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of - # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged. - # - # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing - # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the - # array of strings format for your joins. - # - # class Article < ActiveRecord::Base - # def self.find_with_scope - # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do - # with_scope(:find => limit(10)) do - # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 - # end - # with_scope(:find => where(:author_id => 3)) do - # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 - # end - # end - # end - # end - # - # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method. - # - # class Article < ActiveRecord::Base - # def self.find_with_exclusive_scope - # with_scope(:find => where(:blog_id => 1).limit(1)) do - # with_exclusive_scope(:find => limit(10)) do - # all # => SELECT * from articles LIMIT 10 - # end - # end - # end - # end - # - # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. - def with_scope(scope = {}, action = :merge, &block) - # If another Active Record class has been passed in, get its current scope - scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) - - previous_scope = self.current_scope - - if scope.is_a?(Hash) - # Dup first and second level of hash (method and params). - scope = scope.dup - scope.each do |method, params| - scope[method] = params.dup unless params == true - end - - scope.assert_valid_keys([ :find, :create ]) - relation = construct_finder_arel(scope[:find] || {}) - relation.default_scoped = true unless action == :overwrite - - if previous_scope && previous_scope.create_with_value && scope[:create] - scope_for_create = if action == :merge - previous_scope.create_with_value.merge(scope[:create]) - else - scope[:create] - end - - relation = relation.create_with(scope_for_create) - else - scope_for_create = scope[:create] - scope_for_create ||= previous_scope.create_with_value if previous_scope - relation = relation.create_with(scope_for_create) if scope_for_create - end - - scope = relation - end - - scope = previous_scope.merge(scope) if previous_scope && action == :merge - - self.current_scope = scope - begin - yield - ensure - self.current_scope = previous_scope - end - end - - # Works like with_scope, but discards any nested properties. - def with_exclusive_scope(method_scoping = {}, &block) - if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) } - raise ArgumentError, <<-MSG -New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope: - - User.unscoped.where(:active => true) - -Or call unscoped with a block: - - User.unscoped do - User.where(:active => true).all - end - -MSG - end - with_scope(method_scoping, :overwrite, &block) - end - - def current_scope #:nodoc: - Thread.current["#{self}_current_scope"] - end - - def current_scope=(scope) #:nodoc: - Thread.current["#{self}_current_scope"] = scope - end - - # Use this macro in your model to set a default scope for all operations on - # the model. - # - # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true - # - # The <tt>default_scope</tt> is also applied while creating/building a record. It is not - # applied while updating a record. - # - # Article.new.published # => true - # Article.create.published # => true - # - # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: - # - # class Article < ActiveRecord::Base - # default_scope { where(:published_at => Time.now - 1.week) } - # end - # - # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> - # macro, and it will be called when building the default scope.) - # - # If you use multiple <tt>default_scope</tt> declarations in your model then they will - # be merged together: - # - # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # default_scope where(:rating => 'G') - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' - # - # This is also the case with inheritance and module includes where the parent or module - # defines a <tt>default_scope</tt> and the child or including class defines a second one. - # - # If you need to do more complex things with a default scope, you can alternatively - # define it as a class method: - # - # class Article < ActiveRecord::Base - # def self.default_scope - # # Should return a scope, you can call 'super' here etc. - # end - # end - def default_scope(scope = {}) - scope = Proc.new if block_given? - self.default_scopes = default_scopes + [scope] - end - - def build_default_scope #:nodoc: - if method(:default_scope).owner != Base.singleton_class - evaluate_default_scope { default_scope } - elsif default_scopes.any? - evaluate_default_scope do - default_scopes.inject(relation) do |default_scope, scope| - if scope.is_a?(Hash) - default_scope.apply_finder_options(scope) - elsif !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(scope.call) - else - default_scope.merge(scope) - end - end - end - end - end - - def ignore_default_scope? #:nodoc: - Thread.current["#{self}_ignore_default_scope"] - end - - def ignore_default_scope=(ignore) #:nodoc: - Thread.current["#{self}_ignore_default_scope"] = ignore - end - - # The ignore_default_scope flag is used to prevent an infinite recursion situation where - # a default scope references a scope which has a default scope which references a scope... - def evaluate_default_scope - return if ignore_default_scope? - - begin - self.ignore_default_scope = true - yield - ensure - self.ignore_default_scope = false - end - end - - # Returns the class type of the record using the current module as a prefix. So descendants of - # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. - def compute_type(type_name) - if type_name.match(/^::/) - # If the type is prefixed with a scope operator then we assume that - # the type_name is an absolute reference. - ActiveSupport::Dependencies.constantize(type_name) - else - # Build a list of candidates to search for - candidates = [] - name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } - candidates << type_name - - candidates.each do |candidate| - begin - constant = ActiveSupport::Dependencies.constantize(candidate) - return constant if candidate == constant.to_s - rescue NameError => e - # We don't want to swallow NoMethodError < NameError errors - raise e unless e.instance_of?(NameError) - end - end - - raise NameError, "uninitialized constant #{candidates.first}" - end - end - - # Returns the class descending directly from ActiveRecord::Base or an - # abstract class, if any, in the inheritance hierarchy. - def class_of_active_record_descendant(klass) - if klass == Base || klass.superclass == Base || klass.superclass.abstract_class? - klass - elsif klass.superclass.nil? - raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" - else - class_of_active_record_descendant(klass.superclass) - end - end - - # Accepts an array, hash, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a WHERE clause. - # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" - # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'" - # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition, table_name = self.table_name) - return nil if condition.blank? - - case condition - when Array; sanitize_sql_array(condition) - when Hash; sanitize_sql_hash_for_conditions(condition, table_name) - else condition - end - end - alias_method :sanitize_sql, :sanitize_sql_for_conditions - - # Accepts an array, hash, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a SET clause. - # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'" - def sanitize_sql_for_assignment(assignments) - case assignments - when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments) - else assignments - end - end - - def aggregate_mapping(reflection) - mapping = reflection.options[:mapping] || [reflection.name, reflection.name] - mapping.first.is_a?(Array) ? mapping : [mapping] - end - - # Accepts a hash of SQL conditions and replaces those attributes - # that correspond to a +composed_of+ relationship with their expanded - # aggregate attribute values. - # Given: - # class Person < ActiveRecord::Base - # composed_of :address, :class_name => "Address", - # :mapping => [%w(address_street street), %w(address_city city)] - # end - # Then: - # { :address => Address.new("813 abc st.", "chicago") } - # # => { :address_street => "813 abc st.", :address_city => "chicago" } - def expand_hash_conditions_for_aggregates(attrs) - expanded_attrs = {} - attrs.each do |attr, value| - unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil? - mapping = aggregate_mapping(aggregation) - mapping.each do |field_attr, aggregate_attr| - if mapping.size == 1 && !value.respond_to?(aggregate_attr) - expanded_attrs[field_attr] = value - else - expanded_attrs[field_attr] = value.send(aggregate_attr) - end - end - else - expanded_attrs[attr] = value - end - end - expanded_attrs - end - - # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. - # { :name => "foo'bar", :group_id => 4 } - # # => "name='foo''bar' and group_id= 4" - # { :status => nil, :group_id => [1,2,3] } - # # => "status IS NULL and group_id IN (1,2,3)" - # { :age => 13..18 } - # # => "age BETWEEN 13 AND 18" - # { 'other_records.id' => 7 } - # # => "`other_records`.`id` = 7" - # { :other_records => { :id => 7 } } - # # => "`other_records`.`id` = 7" - # And for value objects on a composed_of relationship: - # { :address => Address.new("123 abc st.", "chicago") } - # # => "address_street='123 abc st.' and address_city='chicago'" - def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) - attrs = expand_hash_conditions_for_aggregates(attrs) - - table = Arel::Table.new(table_name).alias(default_table_name) - PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| - connection.visitor.accept b - }.join(' AND ') - end - alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions - - # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. - # { :status => nil, :group_id => 1 } - # # => "status = NULL , group_id = 1" - def sanitize_sql_hash_for_assignment(attrs) - attrs.map do |attr, value| - "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}" - end.join(', ') - end - - # Accepts an array of conditions. The array has each value - # sanitized and interpolated into the SQL statement. - # ["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) && statement =~ /:\w+/ - replace_named_bind_variables(statement, values.first) - elsif statement.include?('?') - replace_bind_variables(statement, values) - elsif statement.blank? - statement - else - statement % values.collect { |value| connection.quote_string(value.to_s) } - end - end - - alias_method :sanitize_conditions, :sanitize_sql - - def replace_bind_variables(statement, values) #:nodoc: - raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) - bound = values.dup - c = connection - statement.gsub('?') { quote_bound_value(bound.shift, c) } - end - - def replace_named_bind_variables(statement, bind_vars) #:nodoc: - statement.gsub(/(:?):([a-zA-Z]\w*)/) do - if $1 == ':' # skip postgresql casts - $& # return the whole match - elsif bind_vars.include?(match = $2.to_sym) - quote_bound_value(bind_vars[match]) - else - raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" - end - end - end - - def expand_range_bind_variables(bind_vars) #:nodoc: - expanded = [] - - bind_vars.each do |var| - next if var.is_a?(Hash) - - if var.is_a?(Range) - expanded << var.first - expanded << var.last - else - expanded << var - end - end - - expanded - end - - def quote_bound_value(value, c = connection) #:nodoc: - if value.respond_to?(:map) && !value.acts_like?(:string) - if value.respond_to?(:empty?) && value.empty? - c.quote(nil) - else - value.map { |v| c.quote(v) }.join(',') - end - else - c.quote(value) - end - end - - def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc: - unless expected == provided - raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" - end - end - - def encode_quoted_value(value) #:nodoc: - quoted_value = connection.quote(value) - quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) " - quoted_value - end end public @@ -1563,22 +492,6 @@ MSG run_callbacks :initialize end - # Populate +coder+ with attributes about this record that should be - # serialized. The structure of +coder+ defined in this method is - # guaranteed to match the structure of +coder+ passed to the +init_with+ - # method. - # - # Example: - # - # class Post < ActiveRecord::Base - # end - # coder = {} - # Post.new.encode_with(coder) - # coder # => { 'id' => nil, ... } - def encode_with(coder) - coder['attributes'] = attributes - end - # Initialize an empty model object from +coder+. +coder+ must contain # the attributes necessary for initializing an empty model object. For # example: @@ -1606,178 +519,58 @@ MSG self end - # Returns a String, which Action Pack uses for constructing an URL to this - # object. The default implementation returns this record's id as a String, - # or nil if this record's unsaved. - # - # For example, suppose that you have a User model, and that you have a - # <tt>resources :users</tt> route. Normally, +user_path+ will - # construct a path with the user object's 'id' in it: - # - # user = User.find_by_name('Phusion') - # user_path(user) # => "/users/1" - # - # You can override +to_param+ in your model to make +user_path+ construct - # a path using the user's name instead of the user's id: - # - # class User < ActiveRecord::Base - # def to_param # overridden - # name - # end - # end - # - # user = User.find_by_name('Phusion') - # user_path(user) # => "/users/Phusion" - def to_param - # We can't use alias_method here, because method 'id' optimizes itself on the fly. - id && id.to_s # Be sure to stringify the id for routes - end - - # Returns a cache key that can be used to identify this record. - # - # ==== Examples - # - # Product.new.cache_key # => "products/new" - # Product.find(5).cache_key # => "products/5" (updated_at not available) - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) - def cache_key - case - when new_record? - "#{self.class.model_name.cache_key}/new" - when timestamp = self[:updated_at] - timestamp = timestamp.utc.to_s(:number) - "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" - else - "#{self.class.model_name.cache_key}/#{id}" - end - end - - def quoted_id #:nodoc: - quote_value(id, column_for_attribute(self.class.primary_key)) - end - - # Returns true if the given attribute is in the attributes hash - def has_attribute?(attr_name) - @attributes.has_key?(attr_name.to_s) - end - - # Returns an array of names for the attributes available on this object. - def attribute_names - @attributes.keys - end - - # Allows you to set all the attributes at once by passing in a hash with keys - # matching the attribute names (which again matches the column names). - # - # If any attributes are protected by either +attr_protected+ or - # +attr_accessible+ then only settable attributes will be assigned. - # - # class User < ActiveRecord::Base - # attr_protected :is_admin - # end - # - # user = User.new - # user.attributes = { :username => 'Phusion', :is_admin => true } - # user.username # => "Phusion" - # user.is_admin? # => false - def attributes=(new_attributes) - return unless new_attributes.is_a?(Hash) - - assign_attributes(new_attributes) - 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) - # Allows you to set all the attributes for a particular mass-assignment - # security role by passing in a hash of attributes with keys matching - # the attribute names (which again matches the column names) and the role - # name using the :as option. - # - # To bypass mass-assignment security you can use the :without_protection => true - # option. - # - # class User < ActiveRecord::Base - # attr_accessible :name - # attr_accessible :name, :is_admin, :as => :admin - # end - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }) - # user.name # => "Josh" - # user.is_admin? # => false - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) - # user.name # => "Josh" - # user.is_admin? # => true - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) - # user.name # => "Josh" - # user.is_admin? # => true - def assign_attributes(new_attributes, options = {}) - return unless new_attributes + @attributes = cloned_attributes - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] - @mass_assignment_options = options + _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) - unless options[:without_protection] - attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) + @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 - attributes.each do |k, v| - if k.include?("(") - multi_parameter_attributes << [ k, v ] - elsif respond_to?("#{k}=") - send("#{k}=", v) - else - raise(UnknownAttributeError, "unknown attribute: #{k}") - end - end + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @new_record = true - @mass_assignment_options = nil - assign_multiparameter_attributes(multi_parameter_attributes) + ensure_proper_type + populate_with_current_scope_attributes + super end - # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. - def attributes - Hash[@attributes.map { |name, _| [name, read_attribute(name)] }] + # 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 - # Returns an <tt>#inspect</tt>-like string for the value of the - # attribute +attr_name+. String attributes are truncated upto 50 - # characters, and Date and Time attributes are returned in the - # <tt>:db</tt> format. Other attributes return the value of - # <tt>#inspect</tt> without modification. - # - # person = Person.create!(:name => "David Heinemeier Hansson " * 3) + # Populate +coder+ with attributes about this record that should be + # serialized. The structure of +coder+ defined in this method is + # guaranteed to match the structure of +coder+ passed to the +init_with+ + # method. # - # person.attribute_for_inspect(:name) - # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."' + # Example: # - # person.attribute_for_inspect(:created_at) - # # => '"2009-01-12 04:48:57"' - def attribute_for_inspect(attr_name) - value = read_attribute(attr_name) - - if value.is_a?(String) && value.length > 50 - "#{value[0..50]}...".inspect - elsif value.is_a?(Date) || value.is_a?(Time) - %("#{value.to_s(:db)}") - else - value.inspect - end - end - - # 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.nil? || (value.respond_to?(:empty?) && !value.empty?) - end - - # Returns the column object for the named attribute. - def column_for_attribute(name) - self.class.columns_hash[name.to_s] + # class Post < ActiveRecord::Base + # end + # coder = {} + # Post.new.encode_with(coder) + # coder # => { 'id' => nil, ... } + def encode_with(coder) + coder['attributes'] = attributes end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -1822,44 +615,6 @@ MSG end end - # 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 - - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @new_record = true - - ensure_proper_type - populate_with_current_scope_attributes - super - end - # Returns +true+ if the record is read only. Records loaded through joins with piggy-back # attributes will be marked as read only since they cannot be saved. def readonly? @@ -1885,27 +640,24 @@ MSG "#<#{self.class} #{inspection}>" end - protected - def clone_attributes(reader_method = :read_attribute, attributes = {}) - attribute_names.each do |name| - attributes[name] = clone_attribute_value(reader_method, name) + # Hackery to accomodate Syck. Remove for 4.0. + def to_yaml(opts = {}) #:nodoc: + if YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck? + super + else + coder = {} + encode_with(coder) + YAML.quick_emit(self, opts) do |out| + out.map(taguri, to_yaml_style) do |map| + coder.each { |k, v| map.add(k, v) } + end + end end - attributes - end - - def clone_attribute_value(reader_method, attribute_name) - value = send(reader_method, attribute_name) - value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - value end - def mass_assignment_options - @mass_assignment_options ||= {} - end - - def mass_assignment_role - mass_assignment_options[:as] || :default + # Hackery to accomodate Syck. Remove for 4.0. + def yaml_initialize(tag, coder) #:nodoc: + init_with(coder) end private @@ -1922,246 +674,37 @@ MSG nil end - def set_serialized_attributes - sattrs = self.class.serialized_attributes - - sattrs.each do |key, coder| - @attributes[key] = coder.load @attributes[key] if @attributes.key?(key) - end - end - - # Sets the attribute used for single table inheritance to this class name if this is not the - # ActiveRecord::Base descendant. - # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to - # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. - # No such attribute would be set for objects of the Message class in that example. - def ensure_proper_type - klass = self.class - if klass.finder_needs_type_condition? - write_attribute(klass.inheritance_column, klass.sti_name) - end - end - - # The primary key and inheritance column can never be set by mass-assignment for security reasons. - def self.attributes_protected_by_default - default = [ primary_key, inheritance_column ] - default << 'id' unless primary_key.eql? 'id' - default - end - - # Returns a copy of the attributes hash where all the values have been safely quoted for use in - # an Arel insert/update method. - def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) - attrs = {} - klass = self.class - arel_table = klass.arel_table - - attribute_names.each do |name| - if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) - - if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name)) - - value = if coder = klass.serialized_attributes[name] - coder.dump @attributes[name] - else - # FIXME: we need @attributes to be used consistently. - # If the values stored in @attributes were already type - # casted, this code could be simplified - read_attribute(name) - end - - attrs[arel_table[name]] = value - end - end - end - attrs - end - - # Quote strings appropriately for SQL statements. - def quote_value(value, column = nil) - self.class.connection.quote(value, column) - end - - # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done - # by calling new on the column type or aggregation type (through composed_of) object with these parameters. - # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate - # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the - # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, - # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the - # attribute will be set to nil. - def assign_multiparameter_attributes(pairs) - execute_callstack_for_multiparameter_attributes( - extract_callstack_for_multiparameter_attributes(pairs) - ) - end - - def instantiate_time_object(name, values) - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) - Time.zone.local(*values) - else - Time.time_with_datetime_fallback(@@default_timezone, *values) - end - end - - def execute_callstack_for_multiparameter_attributes(callstack) - errors = [] - callstack.each do |name, values_with_empty_parameters| - begin - send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) - rescue => ex - errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name) - end - end - unless errors.empty? - raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" - end - end - - def read_value_from_parameter(name, values_hash_from_param) - klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass - if values_hash_from_param.values.all?{|v|v.nil?} - nil - elsif klass == Time - read_time_parameter_value(name, values_hash_from_param) - elsif klass == Date - read_date_parameter_value(name, values_hash_from_param) - else - read_other_parameter_value(klass, name, values_hash_from_param) - end - end - - def read_time_parameter_value(name, values_hash_from_param) - # If Date bits were not provided, error - raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)} - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) - # If Date bits were provided but blank, then return nil - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - - set_values = (1..max_position).collect{|position| values_hash_from_param[position] } - # If Time bits are not there, then default to 0 - (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]} - instantiate_time_object(name, set_values) - end - - def read_date_parameter_value(name, values_hash_from_param) - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - end - - def read_other_parameter_value(klass, name, values_hash_from_param) - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) - values = (1..max_position).collect do |position| - raise "Missing Parameter" if !values_hash_from_param.has_key?(position) - values_hash_from_param[position] - end - klass.new(*values) - end - - def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) - [values_hash_from_param.keys.max,upper_cap].min - end - - def extract_callstack_for_multiparameter_attributes(pairs) - attributes = { } - - pairs.each do |pair| - multiparameter_name, value = pair - attribute_name = multiparameter_name.split("(").first - attributes[attribute_name] = {} unless attributes.include?(attribute_name) - - parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) - attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value - end - - attributes - end - - def type_cast_attribute_value(multiparameter_name, value) - multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value - end - - def find_parameter_position(multiparameter_name) - multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i - end - - # Returns a comma-separated pair list, like "key1 = val1, key2 = val2". - def comma_pair_list(hash) - hash.map { |k,v| "#{k} = #{v}" }.join(", ") - end - - def quote_columns(quoter, hash) - Hash[hash.map { |name, value| [quoter.quote_column_name(name), value] }] - end - - def quoted_comma_pair_list(quoter, hash) - comma_pair_list(quote_columns(quoter, hash)) - end - - def convert_number_column_value(value) - if value == false - 0 - elsif value == true - 1 - elsif value.is_a?(String) && value.blank? - nil - else - value - end - end - - def populate_with_current_scope_attributes - return unless self.class.scope_attributes? - - self.class.scope_attributes.each do |att,value| - send("#{att}=", value) if respond_to?("#{att}=") - end - end - end - - Base.class_eval do include ActiveRecord::Persistence extend ActiveModel::Naming extend QueryCache::ClassMethods extend ActiveSupport::Benchmarkable extend ActiveSupport::DescendantsTracker + extend Querying + include ReadonlyAttributes + include ModelSchema + extend Translation + include Inheritance + include Scoping + extend DynamicMatchers + include Sanitization + include Integration + include AttributeAssignment include ActiveModel::Conversion include Validations extend CounterCache include Locking::Optimistic, Locking::Pessimistic include AttributeMethods - include AttributeMethods::Read, AttributeMethods::Write, AttributeMethods::BeforeTypeCast, AttributeMethods::Query - include AttributeMethods::PrimaryKey - include AttributeMethods::TimeZoneConversion - include AttributeMethods::Dirty - include ActiveModel::MassAssignmentSecurity include Callbacks, ActiveModel::Observing, Timestamp - include Associations, NamedScope + include Associations include IdentityMap include ActiveModel::SecurePassword + include Explain # AutosaveAssociation needs to be included before Transactions, because we want # #save_with_autosave_associations to be wrapped inside a transaction. include AutosaveAssociation, NestedAttributes include Aggregations, Transactions, Reflection, Serialization, Store - - NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner) - - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, - # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). - # (Alias for the protected read_attribute method). - alias [] read_attribute - - # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. - # (Alias for the protected write_attribute method). - alias []= write_attribute - - public :[], :[]= end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index e32154780a..698da34d26 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,7 +1,6 @@ require 'thread' require 'monitor' require 'set' -require 'active_support/core_ext/module/synchronization' require 'active_support/core_ext/module/deprecation' module ActiveRecord @@ -58,6 +57,8 @@ module ActiveRecord # * +wait_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). class ConnectionPool + include MonitorMixin + attr_accessor :automatic_reconnect attr_reader :spec, :connections @@ -68,23 +69,21 @@ module ActiveRecord # # The default ConnectionPool maximum size is 5. def initialize(spec) + super() + @spec = spec # The cache of reserved connections mapped to threads @reserved_connections = {} - # The mutex used to synchronize pool access - @connection_mutex = Monitor.new - @queue = @connection_mutex.new_cond + @queue = new_cond @timeout = spec.config[:wait_timeout] || 5 # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 @connections = [] - @checked_out = [] @automatic_reconnect = true - @visitor = nil end # Retrieve the connection associated with the current thread, or call @@ -99,7 +98,7 @@ module ActiveRecord # Check to see if there is an active connection in this connection # pool. def active_connection? - @reserved_connections.key? current_connection_id + active_connections.any? end # Signal that the thread is finished with the current connection. @@ -115,7 +114,7 @@ module ActiveRecord # connection when finished. def with_connection connection_id = current_connection_id - fresh_connection = true unless @reserved_connections[connection_id] + fresh_connection = true unless active_connection? yield connection ensure release_connection(connection_id) if fresh_connection @@ -123,41 +122,43 @@ module ActiveRecord # Returns true if a connection has already been opened. def connected? - !@connections.empty? + synchronize { @connections.any? } end # Disconnects all connections in the pool, and clears the pool. def disconnect! - @reserved_connections.each do |name,conn| - checkin conn - end - @reserved_connections = {} - @connections.each do |conn| - conn.disconnect! + synchronize do + @reserved_connections = {} + @connections.each do |conn| + checkin conn + conn.disconnect! + end + @connections = [] end - @connections = [] end # Clears the cache which maps classes. def clear_reloadable_connections! - @reserved_connections.each do |name, conn| - checkin conn - end - @reserved_connections = {} - @connections.each do |conn| - conn.disconnect! if conn.requires_reloading? - end - @connections.delete_if do |conn| - conn.requires_reloading? + synchronize do + @reserved_connections = {} + @connections.each do |conn| + checkin conn + conn.disconnect! if conn.requires_reloading? + end + @connections.delete_if do |conn| + conn.requires_reloading? + end end end # Verify active connections and remove and disconnect connections # associated with stale threads. def verify_active_connections! #:nodoc: - clear_stale_cached_connections! - @connections.each do |connection| - connection.verify! + synchronize do + clear_stale_cached_connections! + @connections.each do |connection| + connection.verify! + end end end @@ -196,7 +197,13 @@ module ActiveRecord t.alive? }.map { |thread| thread.object_id } keys.each do |key| - checkin @reserved_connections[key] + conn = @reserved_connections[key] + ActiveSupport::Deprecation.warn(<<-eowarn) if conn.in_use? +Database connections will not be closed automatically, please close your +database connection at the end of the thread by calling `close` on your +connection. For example: ActiveRecord::Base.connection.close + eowarn + checkin conn @reserved_connections.delete(key) end end @@ -219,22 +226,29 @@ module ActiveRecord # within the timeout period. def checkout # Checkout an available connection - @connection_mutex.synchronize do + synchronize do loop do - conn = if @checked_out.size < @connections.size - checkout_existing_connection - elsif @connections.size < @size - checkout_new_connection - end - return conn if conn + conn = @connections.find { |c| c.lease } + + unless conn + if @connections.size < @size + conn = checkout_new_connection + conn.lease + end + end + + if conn + checkout_and_verify conn + return conn + end @queue.wait(@timeout) - if(@checked_out.size < @connections.size) + if(active_connections.size < @connections.size) next else clear_stale_cached_connections! - if @size == @checked_out.size + if @size == active_connections.size raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it." end end @@ -249,17 +263,14 @@ module ActiveRecord # +conn+: an AbstractAdapter object, which was obtained by earlier by # calling +checkout+ on this pool. def checkin(conn) - @connection_mutex.synchronize do + synchronize do conn.run_callbacks :checkin do - @checked_out.delete conn + conn.expire @queue.signal end end end - synchronize :clear_reloadable_connections!, :verify_active_connections!, - :connected?, :disconnect!, :with => :@connection_mutex - private def new_connection @@ -274,22 +285,21 @@ module ActiveRecord raise ConnectionNotEstablished unless @automatic_reconnect c = new_connection + c.pool = self @connections << c - checkout_and_verify(c) - end - - def checkout_existing_connection - c = (@connections - @checked_out).first - checkout_and_verify(c) + c end def checkout_and_verify(c) c.run_callbacks :checkout do c.verify! - @checked_out << c end c end + + def active_connections + @connections.find_all { |c| c.in_use? } + end end # ConnectionHandler is a collection of ConnectionPool objects. It is used @@ -320,10 +330,12 @@ module ActiveRecord def initialize(pools = {}) @connection_pools = pools + @class_to_pool = {} end def establish_connection(name, spec) - @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec) + @connection_pools[spec] ||= ConnectionAdapters::ConnectionPool.new(spec) + @class_to_pool[name] = @connection_pools[spec] end # Returns true if there are any active connections among the connection @@ -374,16 +386,17 @@ module ActiveRecord # can be used as an argument for establish_connection, for easily # re-establishing the connection. def remove_connection(klass) - pool = @connection_pools.delete(klass.name) + pool = @class_to_pool.delete(klass.name) return nil unless pool + @connection_pools.delete pool.spec pool.automatic_reconnect = false pool.disconnect! pool.spec.config end def retrieve_connection_pool(klass) - pool = @connection_pools[klass.name] + pool = @class_to_pool[klass.name] return pool if pool return nil if ActiveRecord::Base == klass retrieve_connection_pool klass.superclass diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 3d0f146fed..7145dc0692 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -5,6 +5,74 @@ module ActiveRecord def initialize (config, adapter_method) @config, @adapter_method = config, adapter_method end + + ## + # Builds a ConnectionSpecification from user input + class Resolver # :nodoc: + attr_reader :config, :klass, :configurations + + def initialize(config, configurations) + @config = config + @configurations = configurations + end + + def spec + case config + when nil + raise AdapterNotSpecified unless defined?(Rails.env) + resolve_string_connection Rails.env + when Symbol, String + resolve_string_connection config.to_s + when Hash + resolve_hash_connection config + end + end + + private + def resolve_string_connection(spec) # :nodoc: + hash = configurations.fetch(spec) do |k| + connection_url_to_hash(k) + end + + raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash + + resolve_hash_connection hash + end + + def resolve_hash_connection(spec) # :nodoc: + spec = spec.symbolize_keys + + raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) + + begin + require "active_record/connection_adapters/#{spec[:adapter]}_adapter" + rescue LoadError => e + raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace + end + + adapter_method = "#{spec[:adapter]}_connection" + + ConnectionSpecification.new(spec, adapter_method) + end + + def connection_url_to_hash(url) # :nodoc: + config = URI.parse url + adapter = config.scheme + adapter = "postgresql" if adapter == "postgres" + spec = { :adapter => adapter, + :username => config.user, + :password => config.password, + :port => config.port, + :database => config.path.sub(%r{^/},""), + :host => config.host } + spec.reject!{ |_,value| !value } + if config.query + options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys + spec.merge!(options) + end + spec + end + end end ## @@ -55,56 +123,15 @@ module ActiveRecord # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError # may be returned on an error. def self.establish_connection(spec = ENV["DATABASE_URL"]) - case spec - when nil - raise AdapterNotSpecified unless defined?(Rails.env) - establish_connection(Rails.env) - when ConnectionSpecification - self.connection_handler.establish_connection(name, spec) - when Symbol, String - if configuration = configurations[spec.to_s] - establish_connection(configuration) - elsif spec.is_a?(String) && hash = connection_url_to_hash(spec) - establish_connection(hash) - else - raise AdapterNotSpecified, "#{spec} database is not configured" - end - else - spec = spec.symbolize_keys - unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end + resolver = ConnectionSpecification::Resolver.new spec, configurations + spec = resolver.spec - begin - require "active_record/connection_adapters/#{spec[:adapter]}_adapter" - rescue LoadError => e - raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})" - end - - adapter_method = "#{spec[:adapter]}_connection" - unless respond_to?(adapter_method) - raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" - end - - remove_connection - establish_connection(ConnectionSpecification.new(spec, adapter_method)) + unless respond_to?(spec.adapter_method) + raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" end - end - def self.connection_url_to_hash(url) # :nodoc: - config = URI.parse url - adapter = config.scheme - adapter = "postgresql" if adapter == "postgres" - spec = { :adapter => adapter, - :username => config.user, - :password => config.password, - :port => config.port, - :database => config.path.sub(%r{^/},""), - :host => config.host } - spec.reject!{ |_,value| !value } - if config.query - options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys - spec.merge!(options) - end - spec + remove_connection + connection_handler.establish_connection name, spec end class << self 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 dc4a53034b..eb8cff9610 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -130,7 +130,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html + # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL, but not SQLite3. # # It is safe to call this method if a database transaction is already open, @@ -341,7 +341,7 @@ module ActiveRecord # Send a rollback message to all records after they have been rolled back. If rollback # is false, only rollback records since the last save point. - def rollback_transaction_records(rollback) #:nodoc + def rollback_transaction_records(rollback) if rollback records = @_current_transaction_records.flatten @_current_transaction_records.clear @@ -361,7 +361,7 @@ module ActiveRecord end # Send a commit message to all records after they have been committed. - def commit_transaction_records #:nodoc + def commit_transaction_records records = @_current_transaction_records.flatten @_current_transaction_records.clear unless records.blank? diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 6f135b56b5..132ca10f79 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -66,6 +66,7 @@ module ActiveRecord def initialize(base) @columns = [] + @columns_hash = {} @base = base end @@ -86,7 +87,7 @@ module ActiveRecord # Returns a ColumnDefinition for the column with name +name+. def [](name) - @columns.find {|column| column.name.to_s == name.to_s} + @columns_hash[name.to_s] end # Instantiates a new column for the table. @@ -224,28 +225,31 @@ module ActiveRecord # t.references :taggable, :polymorphic => { :default => 'Photo' } # end def column(name, type, options = {}) - column = self[name] || ColumnDefinition.new(@base, name, type) - if options[:limit] - column.limit = options[:limit] - elsif native[type.to_sym].is_a?(Hash) - column.limit = native[type.to_sym][:limit] + name = name.to_s + type = type.to_sym + + column = self[name] || new_column_definition(@base, name, type) + + limit = options.fetch(:limit) do + native[type][:limit] if native[type].is_a?(Hash) end + + column.limit = limit column.precision = options[:precision] - column.scale = options[:scale] - column.default = options[:default] - column.null = options[:null] - @columns << column unless @columns.include? column + column.scale = options[:scale] + column.default = options[:default] + column.null = options[:null] self end %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| class_eval <<-EOV, __FILE__, __LINE__ + 1 - def #{column_type}(*args) # def string(*args) - options = args.extract_options! # options = args.extract_options! - column_names = args # column_names = args - # - column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) } - end # end + def #{column_type}(*args) # def string(*args) + options = args.extract_options! # options = args.extract_options! + column_names = args # column_names = args + type = :'#{column_type}' # type = :string + column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) } + end # end EOV end @@ -275,9 +279,16 @@ module ActiveRecord end private - def native - @base.native_database_types - end + def new_column_definition(base, name, type) + definition = ColumnDefinition.new base, name, type + @columns << definition + @columns_hash[name] = definition + definition + end + + def native + @base.native_database_types + end end # Represents an SQL table in an abstract way for updating a table. @@ -453,13 +464,13 @@ module ActiveRecord def #{column_type}(*args) # def string(*args) options = args.extract_options! # options = args.extract_options! column_names = args # column_names = args - # + type = :'#{column_type}' # type = :string column_names.each do |name| # column_names.each do |name| - column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string') + column = ColumnDefinition.new(@base, name.to_s, type) # column = ColumnDefinition.new(@base, name, type) if options[:limit] # if options[:limit] column.limit = options[:limit] # column.limit = options[:limit] - elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash) - column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit] + elsif native[type].is_a?(Hash) # elsif native[type].is_a?(Hash) + column.limit = native[type][:limit] # column.limit = native[type][:limit] end # end column.precision = options[:precision] # column.precision = options[:precision] column.scale = options[:scale] # column.scale = options[:scale] diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index faa42e2d19..ccbeba061d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -16,8 +16,6 @@ module ActiveRecord table_name[0...table_alias_length].gsub(/\./, '_') end - # def tables(name = nil) end - # Checks to see if the table +table_name+ exists on the database. # # === Example @@ -114,7 +112,7 @@ module ActiveRecord # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. # # Also note that this just sets the primary key in the table. You additionally - # need to configure the primary key in the model via the +set_primary_key+ macro. + # need to configure the primary key in the model via +self.primary_key=+. # Models do NOT auto-detect the primary key from their table definition. # # [<tt>:options</tt>] diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 75e568b557..5a2493f69d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_support/deprecation' require 'active_record/connection_adapters/schema_cache' +require 'monitor' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -48,21 +49,42 @@ module ActiveRecord include DatabaseLimits include QueryCache include ActiveSupport::Callbacks + include MonitorMixin define_callbacks :checkout, :checkin - attr_accessor :visitor - attr_reader :schema_cache - - def initialize(connection, logger = nil) #:nodoc: - @active = nil - @connection, @logger = connection, logger + attr_accessor :visitor, :pool + attr_reader :schema_cache, :last_use, :in_use + alias :in_use? :in_use + + def initialize(connection, logger = nil, pool = nil) #:nodoc: + super() + + @active = nil + @connection = connection + @in_use = false + @instrumenter = ActiveSupport::Notifications.instrumenter + @last_use = false + @logger = logger + @open_transactions = 0 + @pool = pool + @query_cache = Hash.new { |h,sql| h[sql] = {} } @query_cache_enabled = false - @query_cache = Hash.new { |h,sql| h[sql] = {} } - @open_transactions = 0 - @instrumenter = ActiveSupport::Notifications.instrumenter - @visitor = nil - @schema_cache = SchemaCache.new self + @schema_cache = SchemaCache.new self + @visitor = nil + end + + def lease + synchronize do + unless in_use + @in_use = true + @last_use = Time.now + end + end + end + + def expire + @in_use = false end # Returns the human-readable name of the adapter. Use mixed case - one @@ -120,6 +142,12 @@ module ActiveRecord false end + # Does this adapter support explain? As of this writing sqlite3, + # mysql2, and postgresql are the only ones that do. + def supports_explain? + false + end + # QUOTING ================================================== # Override to return the quoted table name. Defaults to column quoting. @@ -236,6 +264,11 @@ module ActiveRecord "active_record_#{open_transactions}" end + # Check the connection back in to the connection pool + def close + pool.checkin self + end + protected def log(sql, name = "SQL", binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index f143fd348e..560773ca86 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -152,7 +152,7 @@ module ActiveRecord true end - # Technically MySQL allows to create indexes with the sort order syntax + # Technically MySQL allows to create indexes with the sort order syntax # but at the moment (5.5) it doesn't yet implement them def supports_index_sort_order? true @@ -225,80 +225,6 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== - def explain(arel) - sql = "EXPLAIN #{to_sql(arel)}" - start = Time.now - result = exec_query(sql, 'EXPLAIN') - elapsed = Time.now - start - - ExplainPrettyPrinter.new.pp(result, elapsed) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # MySQL shell: - # - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | - # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # 2 rows in set (0.00 sec) - # - # This is an exercise in Ruby hyperrealism :). - def pp(result, elapsed) - widths = compute_column_widths(result) - separator = build_separator(widths) - - pp = [] - - pp << separator - pp << build_cells(result.columns, widths) - pp << separator - - result.rows.each do |row| - pp << build_cells(row, widths) - end - - pp << separator - pp << build_footer(result.rows.length, elapsed) - - pp.join("\n") + "\n" - end - - private - - def compute_column_widths(result) - [].tap do |widths| - result.columns.each_with_index do |column, i| - cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} - widths << cells_in_column.map(&:length).max - end - end - end - - def build_separator(widths) - padding = 1 - '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' - end - - def build_cells(items, widths) - cells = [] - items.each_with_index do |item, i| - item = 'NULL' if item.nil? - justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' - cells << item.to_s.send(justifier, widths[i]) - end - '| ' + cells.join(' | ') + ' |' - end - - def build_footer(nrows, elapsed) - rows_label = nrows == 1 ? 'row' : 'rows' - "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed - end - end - # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) if name == :skip_logging @@ -437,8 +363,10 @@ module ActiveRecord show_variable 'collation_database' end - def tables(name = nil, database = nil) #:nodoc: - sql = ["SHOW TABLES", database].compact.join(' IN ') + def tables(name = nil, database = nil, like = nil) #:nodoc: + sql = "SHOW TABLES " + sql << "IN #{database} " if database + sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| result.collect { |field| field.first } @@ -446,7 +374,8 @@ module ActiveRecord end def table_exists?(name) - return true if super + return false unless name + return true if tables(nil, nil, name).any? name = name.to_s schema, table = name.split('.', 2) @@ -456,7 +385,7 @@ module ActiveRecord schema = nil end - tables(nil, schema).include? table + tables(nil, schema, table).any? end # Returns an array of indexes for the given table. @@ -573,9 +502,14 @@ module ActiveRecord # Returns a table's primary key and belonging sequence. def pk_and_sequence_for(table) - execute_and_free("SHOW INDEX FROM #{quote_table_name(table)} WHERE Key_name = 'PRIMARY'", 'SCHEMA') do |result| - keys = each_hash(result).map { |row| row[:Column_name] } - keys.length == 1 ? [keys.first, nil] : nil + execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result| + create_table = each_hash(result).first[:"Create Table"] + if create_table.to_s =~ /PRIMARY KEY\s+\((.+)\)/ + keys = $1.split(",").map { |key| key.gsub(/`/, "") } + keys.length == 1 ? [keys.first, nil] : nil + else + nil + end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 971f3c35f3..626571a948 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,6 +1,6 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' -gem 'mysql2', '~> 0.3.6' +gem 'mysql2', '~> 0.3.10' require 'mysql2' module ActiveRecord @@ -35,6 +35,10 @@ module ActiveRecord configure_connection end + def supports_explain? + true + end + # HELPER METHODS =========================================== def each_hash(result) # :nodoc: @@ -93,6 +97,80 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel)}" + start = Time.now + result = exec_query(sql, 'EXPLAIN', binds) + elapsed = Time.now - start + + ExplainPrettyPrinter.new.pp(result, elapsed) + end + + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # MySQL shell: + # + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | + # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # 2 rows in set (0.00 sec) + # + # This is an exercise in Ruby hyperrealism :). + def pp(result, elapsed) + widths = compute_column_widths(result) + separator = build_separator(widths) + + pp = [] + + pp << separator + pp << build_cells(result.columns, widths) + pp << separator + + result.rows.each do |row| + pp << build_cells(row, widths) + end + + pp << separator + pp << build_footer(result.rows.length, elapsed) + + pp.join("\n") + "\n" + end + + private + + def compute_column_widths(result) + [].tap do |widths| + result.columns.each_with_index do |column, i| + cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} + widths << cells_in_column.map(&:length).max + end + end + end + + def build_separator(widths) + padding = 1 + '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' + end + + def build_cells(items, widths) + cells = [] + items.each_with_index do |item, i| + item = 'NULL' if item.nil? + justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' + cells << item.to_s.send(justifier, widths[i]) + end + '| ' + cells.join(' | ') + ' |' + end + + def build_footer(nrows, elapsed) + rows_label = nrows == 1 ? 'row' : 'rows' + "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed + end + end + # FIXME: re-enable the following once a "better" query_cache solution is in core # # The overrides below perform much better than the originals in AbstractAdapter diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 852debcdde..6b742ed858 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -390,6 +390,11 @@ module ActiveRecord true end + # Returns true. + def supports_explain? + true + end + # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length @table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i @@ -514,9 +519,9 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== - def explain(arel) + def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel)}" - ExplainPrettyPrinter.new.pp(exec_query(sql)) + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) end class ExplainPrettyPrinter # :nodoc: @@ -1239,7 +1244,7 @@ module ActiveRecord if match_data rest = name[match_data[0].length, name.length] rest = rest[1, rest.length] if rest.start_with? "." - [match_data[1], (rest.any? ? rest : nil)] + [match_data[1], (rest.length > 0 ? rest : nil)] end end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index b14b37ce89..4e8932a695 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -2,22 +2,14 @@ module ActiveRecord module ConnectionAdapters class SchemaCache attr_reader :columns, :columns_hash, :primary_keys, :tables - attr_reader :column_defaults attr_reader :connection def initialize(conn) @connection = conn - @tables = {} + @tables = {} - @columns = Hash.new do |h, table_name| - h[table_name] = - # Fetch a list of columns - conn.columns(table_name, "#{table_name} Columns").tap do |cs| - # set primary key information - cs.each do |column| - column.primary = column.name == primary_keys[table_name] - end - end + @columns = Hash.new do |h, table_name| + h[table_name] = conn.columns(table_name, "#{table_name} Columns") end @columns_hash = Hash.new do |h, table_name| @@ -26,15 +18,8 @@ module ActiveRecord }] end - @column_defaults = Hash.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| - [col.name, col.default] - }] - end - @primary_keys = Hash.new do |h, table_name| - h[table_name] = table_exists?(table_name) ? - conn.primary_key(table_name) : 'id' + h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil end end @@ -42,21 +27,14 @@ module ActiveRecord def table_exists?(name) return @tables[name] if @tables.key? name - connection.tables.each { |table| @tables[table] = true } - @tables[name] = connection.table_exists?(name) if !@tables.key?(name) - - @tables[name] + @tables[name] = connection.table_exists?(name) end - # Clears out internal caches: - # - # * columns - # * columns_hash - # * tables + # Clears out internal caches def clear! @columns.clear @columns_hash.clear - @column_defaults.clear + @primary_keys.clear @tables.clear end @@ -64,8 +42,8 @@ module ActiveRecord def clear_table_cache!(table_name) @columns.delete table_name @columns_hash.delete table_name - @column_defaults.delete table_name @primary_keys.delete table_name + @tables.delete table_name end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 0a0da0b5d3..11bb457d03 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,6 +1,6 @@ require 'active_record/connection_adapters/sqlite_adapter' -gem 'sqlite3', '~> 1.3.4' +gem 'sqlite3', '~> 1.3.5' require 'sqlite3' module ActiveRecord @@ -47,11 +47,7 @@ module ActiveRecord # Returns the current database encoding format as a string, eg: 'UTF-8' def encoding - if @connection.respond_to?(:encoding) - @connection.encoding.to_s - else - @connection.execute('PRAGMA encoding')[0]['encoding'] - end + @connection.encoding.to_s 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 c11f82a33f..55818b3fbf 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -122,6 +122,11 @@ module ActiveRecord true end + # Returns true. + def supports_explain? + true + end + def requires_reloading? true end @@ -219,9 +224,9 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== - def explain(arel) + def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN')) + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) end class ExplainPrettyPrinter @@ -324,18 +329,23 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def tables(name = 'SCHEMA') #:nodoc: + def tables(name = 'SCHEMA', table_name = nil) #:nodoc: sql = <<-SQL SELECT name FROM sqlite_master WHERE type = 'table' AND NOT name = 'sqlite_sequence' SQL + sql << " AND name = #{quote_table_name(table_name)}" if table_name exec_query(sql, name).map do |row| row['name'] end end + def table_exists?(name) + name && tables('SCHEMA', name).any? + end + # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+. def columns(table_name, name = nil) #:nodoc: table_structure(table_name).map do |field| diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb new file mode 100644 index 0000000000..e9068089f0 --- /dev/null +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -0,0 +1,79 @@ +module ActiveRecord + module DynamicMatchers + def respond_to?(method_id, include_private = false) + if match = DynamicFinderMatch.match(method_id) + return true if all_attributes_exists?(match.attribute_names) + elsif match = DynamicScopeMatch.match(method_id) + return true if all_attributes_exists?(match.attribute_names) + end + + super + end + + private + + # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and + # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders + # section at the top of this file for more detailed information. + # + # It's even possible to use all the additional parameters to +find+. For example, the + # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. + # + # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it + # is first invoked, so that future attempts to use it do not run through method_missing. + def method_missing(method_id, *arguments, &block) + if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)) + attribute_names = match.attribute_names + super unless all_attributes_exists?(attribute_names) + if arguments.size < attribute_names.size + method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" + backtrace = [method_trace] + caller + raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace + end + if match.respond_to?(:scope?) && match.scope? + self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 + def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) + attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)] + # + scoped(:conditions => attributes) # scoped(:conditions => attributes) + end # end + METHOD + send(method_id, *arguments) + elsif match.finder? + options = arguments.extract_options! + relation = options.any? ? scoped(options) : scoped + relation.send :find_by_attributes, match, attribute_names, *arguments, &block + elsif match.instantiator? + scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block + end + else + super + end + end + + # Similar in purpose to +expand_hash_conditions_for_aggregates+. + def expand_attribute_names_for_aggregates(attribute_names) + attribute_names.map { |attribute_name| + unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? + aggregate_mapping(aggregation).map do |field_attr, _| + field_attr.to_sym + end + else + attribute_name.to_sym + end + }.flatten + end + + def all_attributes_exists?(attribute_names) + (expand_attribute_names_for_aggregates(attribute_names) - + column_methods_hash.keys).empty? + end + + def aggregate_mapping(reflection) + mapping = reflection.options[:mapping] || [reflection.name, reflection.name] + mapping.first.is_a?(Array) ? mapping : [mapping] + end + + + end +end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb new file mode 100644 index 0000000000..c9e85391cd --- /dev/null +++ b/activerecord/lib/active_record/explain.rb @@ -0,0 +1,85 @@ +require 'active_support/concern' + +module ActiveRecord + module Explain + extend ActiveSupport::Concern + + included do + # If a query takes longer than these many seconds we log its query plan + # automatically. nil disables this feature. + class_attribute :auto_explain_threshold_in_seconds, :instance_writer => false + self.auto_explain_threshold_in_seconds = nil + end + + module ClassMethods + # If auto explain is enabled, this method triggers EXPLAIN logging for the + # queries triggered by the block if it takes more than the threshold as a + # whole. That is, the threshold is not checked against each individual + # query, but against the duration of the entire block. This approach is + # convenient for relations. + # + # The available_queries_for_explain thread variable collects the queries + # to be explained. If the value is nil, it means queries are not being + # currently collected. A false value indicates collecting is turned + # off. Otherwise it is an array of queries. + def logging_query_plan # :nodoc: + threshold = auto_explain_threshold_in_seconds + current = Thread.current + if threshold && current[:available_queries_for_explain].nil? + begin + queries = current[:available_queries_for_explain] = [] + start = Time.now + result = yield + logger.warn(exec_explain(queries)) if Time.now - start > threshold + result + ensure + current[:available_queries_for_explain] = nil + end + else + yield + end + end + + # Relation#explain needs to be able to collect the queries regardless of + # whether auto explain is enabled. This method serves that purpose. + def collecting_queries_for_explain # :nodoc: + current = Thread.current + original, current[:available_queries_for_explain] = current[:available_queries_for_explain], [] + return yield, current[:available_queries_for_explain] + ensure + # Note that the return value above does not depend on this assigment. + current[:available_queries_for_explain] = original + end + + # Makes the adapter execute EXPLAIN for the tuples of queries and bindings. + # Returns a formatted string ready to be logged. + def exec_explain(queries) # :nodoc: + queries && queries.map do |sql, bind| + [].tap do |msg| + msg << "EXPLAIN for: #{sql}" + unless bind.empty? + bind_msg = bind.map {|col, val| [col.name, val]}.inspect + msg.last << " #{bind_msg}" + end + msg << connection.explain(sql, bind) + end.join("\n") + end.join("\n") + end + + # Silences automatic EXPLAIN logging for the duration of the block. + # + # This has high priority, no EXPLAINs will be run even if downwards + # the threshold is set to 0. + # + # As the name of the method suggests this only applies to automatic + # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run. + def silence_auto_explain + current = Thread.current + original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false + yield + ensure + current[:available_queries_for_explain] = original + end + end + end +end diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb new file mode 100644 index 0000000000..fc76410499 --- /dev/null +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -0,0 +1,21 @@ +require 'active_support/notifications' + +module ActiveRecord + class ExplainSubscriber # :nodoc: + def call(*args) + if queries = Thread.current[:available_queries_for_explain] + payload = args.last + queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) + end + end + + # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on + # our own EXPLAINs now matter how loopingly beautiful that would be. + IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN) + def ignore_payload?(payload) + payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) + end + + ActiveSupport::Notifications.subscribe("sql.active_record", new) + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index cad9417216..d8340bf1c9 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -22,8 +22,6 @@ else end end -class FixturesFileNotFound < StandardError; end - module ActiveRecord # \Fixtures are a way of organizing data that you want to test against; in short, sample data. # @@ -644,14 +642,6 @@ module ActiveRecord end def read_fixture_files - if ::File.file?(yaml_file_path) - read_yaml_fixture_files - else - raise FixturesFileNotFound, "Could not find #{yaml_file_path}" - end - end - - def read_yaml_fixture_files yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path] @@ -832,6 +822,7 @@ module ActiveRecord end @fixture_cache = {} + @fixture_connections = [] @@already_loaded_fixtures ||= {} # Load fixtures once and begin transaction. diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb new file mode 100644 index 0000000000..de9461982a --- /dev/null +++ b/activerecord/lib/active_record/inheritance.rb @@ -0,0 +1,167 @@ +require 'active_support/concern' + +module ActiveRecord + module Inheritance + extend ActiveSupport::Concern + + included do + # Determine whether to store the full constant name including namespace when using STI + class_attribute :store_full_sti_class + self.store_full_sti_class = true + end + + module ClassMethods + # True if this isn't a concrete subclass needing a STI type condition. + def descends_from_active_record? + if superclass.abstract_class? + superclass.descends_from_active_record? + else + superclass == Base || !columns_hash.include?(inheritance_column) + end + end + + def finder_needs_type_condition? #:nodoc: + # This is like this because benchmarking justifies the strange :false stuff + :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) + end + + def symbolized_base_class + @symbolized_base_class ||= base_class.to_s.to_sym + end + + def symbolized_sti_name + @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class + end + + # Returns the base AR subclass that this class descends from. If A + # extends AR::Base, A.base_class will return A. If B descends from A + # through some arbitrarily deep hierarchy, B.base_class will return A. + # + # If B < A and C < B and if A is an abstract_class then both B.base_class + # and C.base_class would return B as the answer since A is an abstract_class. + def base_class + class_of_active_record_descendant(self) + end + + # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). + attr_accessor :abstract_class + + # Returns whether this class is an abstract class or not. + def abstract_class? + defined?(@abstract_class) && @abstract_class == true + end + + def sti_name + store_full_sti_class ? name : name.demodulize + end + + # Finder methods must instantiate through this method to work with the + # single-table inheritance model that makes it possible to create + # objects of different types from the same table. + def instantiate(record) + sti_class = find_sti_class(record[inheritance_column]) + record_id = sti_class.primary_key && record[sti_class.primary_key] + + if ActiveRecord::IdentityMap.enabled? && record_id + if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? + record_id = record_id.to_i + end + if instance = IdentityMap.get(sti_class, record_id) + instance.reinit_with('attributes' => record) + else + instance = sti_class.allocate.init_with('attributes' => record) + IdentityMap.add(instance) + end + else + instance = sti_class.allocate.init_with('attributes' => record) + end + + instance + end + + protected + + # Returns the class descending directly from ActiveRecord::Base or an + # abstract class, if any, in the inheritance hierarchy. + def class_of_active_record_descendant(klass) + if klass == Base || klass.superclass == Base || klass.superclass.abstract_class? + klass + elsif klass.superclass.nil? + raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" + else + class_of_active_record_descendant(klass.superclass) + end + end + + # Returns the class type of the record using the current module as a prefix. So descendants of + # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. + def compute_type(type_name) + if type_name.match(/^::/) + # If the type is prefixed with a scope operator then we assume that + # the type_name is an absolute reference. + ActiveSupport::Dependencies.constantize(type_name) + else + # Build a list of candidates to search for + candidates = [] + name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } + candidates << type_name + + candidates.each do |candidate| + begin + constant = ActiveSupport::Dependencies.constantize(candidate) + return constant if candidate == constant.to_s + rescue NameError => e + # We don't want to swallow NoMethodError < NameError errors + raise e unless e.instance_of?(NameError) + end + end + + raise NameError, "uninitialized constant #{candidates.first}" + end + end + + private + + def find_sti_class(type_name) + if type_name.blank? || !columns_hash.include?(inheritance_column) + self + else + begin + if store_full_sti_class + ActiveSupport::Dependencies.constantize(type_name) + else + compute_type(type_name) + end + rescue NameError + raise SubclassNotFound, + "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + + "Please rename this column if you didn't intend it to be used for storing the inheritance class " + + "or overwrite #{name}.inheritance_column to use another column for that information." + end + end + end + + def type_condition(table = arel_table) + sti_column = table[inheritance_column.to_sym] + sti_names = ([self] + descendants).map { |model| model.sti_name } + + sti_column.in(sti_names) + end + end + + private + + # Sets the attribute used for single table inheritance to this class name if this is not the + # ActiveRecord::Base descendant. + # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to + # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. + # No such attribute would be set for objects of the Message class in that example. + def ensure_proper_type + klass = self.class + if klass.finder_needs_type_condition? + write_attribute(klass.inheritance_column, klass.sti_name) + end + end + end +end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb new file mode 100644 index 0000000000..2c42f4cca5 --- /dev/null +++ b/activerecord/lib/active_record/integration.rb @@ -0,0 +1,49 @@ +module ActiveRecord + module Integration + # Returns a String, which Action Pack uses for constructing an URL to this + # object. The default implementation returns this record's id as a String, + # or nil if this record's unsaved. + # + # For example, suppose that you have a User model, and that you have a + # <tt>resources :users</tt> route. Normally, +user_path+ will + # construct a path with the user object's 'id' in it: + # + # user = User.find_by_name('Phusion') + # user_path(user) # => "/users/1" + # + # You can override +to_param+ in your model to make +user_path+ construct + # a path using the user's name instead of the user's id: + # + # class User < ActiveRecord::Base + # def to_param # overridden + # name + # end + # end + # + # user = User.find_by_name('Phusion') + # user_path(user) # => "/users/Phusion" + def to_param + # We can't use alias_method here, because method 'id' optimizes itself on the fly. + id && id.to_s # Be sure to stringify the id for routes + end + + # Returns a cache key that can be used to identify this record. + # + # ==== Examples + # + # Product.new.cache_key # => "products/new" + # Product.find(5).cache_key # => "products/5" (updated_at not available) + # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + def cache_key + case + when new_record? + "#{self.class.model_name.cache_key}/new" + when timestamp = self[:updated_at] + timestamp = timestamp.utc.to_s(:number) + "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" + else + "#{self.class.model_name.cache_key}/#{id}" + end + end + end +end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 1a29ded787..ce0a165660 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -51,10 +51,6 @@ module ActiveRecord included do cattr_accessor :lock_optimistically, :instance_writer => false self.lock_optimistically = true - - class << self - alias_method :locking_column=, :set_locking_column - end end def locking_enabled? #:nodoc: @@ -69,7 +65,7 @@ module ActiveRecord end def attributes_from_column_definition - result = super + result = self.class.column_defaults.dup # If the locking column has no default value set, # start the lock version at zero. Note we can't use @@ -148,15 +144,24 @@ module ActiveRecord lock_optimistically && columns_hash[locking_column] end + def locking_column=(value) + @original_locking_column = @locking_column if defined?(@locking_column) + @locking_column = value.to_s + end + # Set the column to use for optimistic locking. Defaults to +lock_version+. def set_locking_column(value = nil, &block) - define_attr_method :locking_column, value, &block - value + deprecated_property_setter :locking_column, value, block end # The version column used for optimistic locking. Defaults to +lock_version+. def locking_column - reset_locking_column + reset_locking_column unless defined?(@locking_column) + @locking_column + end + + def original_locking_column #:nodoc: + deprecated_original_property_getter :locking_column end # Quote the column name used for optimistic locking. @@ -166,7 +171,7 @@ module ActiveRecord # Reset the column used for optimistic locking back to the +lock_version+ default. def reset_locking_column - set_locking_column DEFAULT_LOCKING_COLUMN + self.locking_column = DEFAULT_LOCKING_COLUMN end # Make sure the lock version column gets updated when counters are diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 3a015ee8c2..a25f2c7bca 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -26,9 +26,9 @@ module ActiveRecord return if 'SCHEMA' == payload[:name] - name = '%s (%.1fms)' % [payload[:name], event.duration] - sql = payload[:sql].squeeze(' ') - binds = nil + name = '%s (%.1fms)' % [payload[:name], event.duration] + sql = payload[:sql].squeeze(' ') + binds = nil unless (payload[:binds] || []).empty? binds = " " + payload[:binds].map { |col,v| diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index d70c7d1d34..46464783fd 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -302,7 +302,7 @@ module ActiveRecord # # class TenderloveMigration < ActiveRecord::Migration # def change - # create_table(:horses) do + # create_table(:horses) do |t| # t.column :content, :text # t.column :remind_at, :datetime # end @@ -443,6 +443,7 @@ module ActiveRecord say_with_time "#{method}(#{arg_list})" do unless arguments.empty? || method == :execute arguments[0] = Migrator.proper_table_name(arguments.first) + arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table end return super unless connection.respond_to?(method) connection.send(method, *arguments, &block) @@ -456,26 +457,28 @@ module ActiveRecord destination_migrations = ActiveRecord::Migrator.migrations(destination) last = destination_migrations.last - sources.each do |name, path| + sources.each do |scope, path| source_migrations = ActiveRecord::Migrator.migrations(path) source_migrations.each do |migration| source = File.read(migration.filename) - source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}" + source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}" 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] + if options[:on_skip] && duplicate.scope != scope.to_s + options[:on_skip].call(scope, migration) + end next end migration.version = next_migration_number(last ? last.version + 1 : 0).to_i - new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb") + new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.#{scope}.rb") old_path, migration.filename = migration.filename, new_path last = migration - FileUtils.cp(old_path, migration.filename) + File.open(migration.filename, "w") { |f| f.write source } copied << migration - options[:on_copy].call(name, migration, old_path) if options[:on_copy] + options[:on_copy].call(scope, migration, old_path) if options[:on_copy] destination_migrations << migration end end @@ -494,9 +497,9 @@ module ActiveRecord # MigrationProxy is used to defer loading of the actual migration classes # until they are needed - class MigrationProxy < Struct.new(:name, :version, :filename) + class MigrationProxy < Struct.new(:name, :version, :filename, :scope) - def initialize(name, version, filename) + def initialize(name, version, filename, scope) super @migration = nil end @@ -525,16 +528,16 @@ module ActiveRecord attr_writer :migrations_paths alias :migrations_path= :migrations_paths= - def migrate(migrations_paths, target_version = nil) + def migrate(migrations_paths, target_version = nil, &block) case when target_version.nil? - up(migrations_paths, target_version) + up(migrations_paths, target_version, &block) when current_version == 0 && target_version == 0 [] when current_version > target_version - down(migrations_paths, target_version) + down(migrations_paths, target_version, &block) else - up(migrations_paths, target_version) + up(migrations_paths, target_version, &block) end end @@ -546,12 +549,12 @@ module ActiveRecord move(:up, migrations_paths, steps) end - def up(migrations_paths, target_version = nil) - self.new(:up, migrations_paths, target_version).migrate + def up(migrations_paths, target_version = nil, &block) + self.new(:up, migrations_paths, target_version).migrate(&block) end - def down(migrations_paths, target_version = nil) - self.new(:down, migrations_paths, target_version).migrate + def down(migrations_paths, target_version = nil, &block) + self.new(:down, migrations_paths, target_version).migrate(&block) end def run(direction, migrations_paths, target_version) @@ -591,15 +594,16 @@ module ActiveRecord migrations_paths.first end - def migrations(paths) + def migrations(paths, subdirectories = true) paths = Array.wrap(paths) - files = Dir[*paths.map { |p| "#{p}/[0-9]*_*.rb" }] + glob = subdirectories ? "**/" : "" + files = Dir[*paths.map { |p| "#{p}/#{glob}[0-9]*_*.rb" }] seen = Hash.new false migrations = files.map do |file| - version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first + version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first raise IllegalMigrationNameError.new(file) unless version version = version.to_i @@ -610,7 +614,7 @@ module ActiveRecord seen[version] = seen[name] = true - MigrationProxy.new(name, version, file) + MigrationProxy.new(name, version, file, scope) end migrations.sort_by(&:version) @@ -653,7 +657,7 @@ module ActiveRecord end end - def migrate + def migrate(&block) current = migrations.detect { |m| m.version == current_version } target = migrations.detect { |m| m.version == @target_version } @@ -670,6 +674,10 @@ module ActiveRecord ran = [] runnable.each do |migration| + if block && !block.call(migration) + next + end + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger seen = migrated.include?(migration.version.to_i) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb new file mode 100644 index 0000000000..36417d89f7 --- /dev/null +++ b/activerecord/lib/active_record/model_schema.rb @@ -0,0 +1,362 @@ +require 'active_support/concern' + +module ActiveRecord + module ModelSchema + extend ActiveSupport::Concern + + included do + ## + # :singleton-method: + # Accessor for the prefix type that will be prepended to every primary key column name. + # The options are :table_name and :table_name_with_underscore. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the + # latter is specified, the Product class will look for "product_id" instead of "id". Remember + # that this is a global setting for all Active Records. + cattr_accessor :primary_key_prefix_type, :instance_writer => false + self.primary_key_prefix_type = nil + + ## + # :singleton-method: + # Accessor for the name of the prefix string to prepend to every table name. So if set + # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people", + # etc. This is a convenient way of creating a namespace for tables in a shared database. + # By default, the prefix is the empty string. + # + # If you are organising your models within modules you can add a prefix to the models within + # a namespace by defining a singleton method in the parent module called table_name_prefix which + # returns your chosen prefix. + class_attribute :table_name_prefix, :instance_writer => false + self.table_name_prefix = "" + + ## + # :singleton-method: + # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", + # "people_basecamp"). By default, the suffix is the empty string. + class_attribute :table_name_suffix, :instance_writer => false + self.table_name_suffix = "" + + ## + # :singleton-method: + # Indicates whether table names should be the pluralized versions of the corresponding class names. + # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. + # See table_name for the full rules on table/class naming. This is true, by default. + class_attribute :pluralize_table_names, :instance_writer => false + self.pluralize_table_names = true + end + + module ClassMethods + # Guesses the table name (in forced lower-case) based on the name of the class in the + # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy + # looks like: Reply < Message < ActiveRecord::Base, then Message is used + # to guess the table name even when called on Reply. The rules used to do the guess + # are handled by the Inflector class in Active Support, which knows almost all common + # English inflections. You can add new inflections in config/initializers/inflections.rb. + # + # Nested classes are given table names prefixed by the singular form of + # the parent's table name. Enclosing modules are not considered. + # + # ==== Examples + # + # class Invoice < ActiveRecord::Base + # end + # + # file class table_name + # invoice.rb Invoice invoices + # + # class Invoice < ActiveRecord::Base + # class Lineitem < ActiveRecord::Base + # end + # end + # + # file class table_name + # invoice.rb Invoice::Lineitem invoice_lineitems + # + # module Invoice + # class Lineitem < ActiveRecord::Base + # end + # end + # + # file class table_name + # invoice/lineitem.rb Invoice::Lineitem lineitems + # + # Additionally, the class-level +table_name_prefix+ is prepended and the + # +table_name_suffix+ is appended. So if you have "myapp_" as a prefix, + # the table name guess for an Invoice class becomes "myapp_invoices". + # Invoice::Lineitem becomes "myapp_invoice_lineitems". + # + # You can also set your own table name explicitly: + # + # class Mouse < ActiveRecord::Base + # self.table_name = "mice" + # end + # + # Alternatively, you can override the table_name method to define your + # own computation. (Possibly using <tt>super</tt> to manipulate the default + # table name.) Example: + # + # class Post < ActiveRecord::Base + # def self.table_name + # "special_" + super + # end + # end + # Post.table_name # => "special_posts" + def table_name + reset_table_name unless defined?(@table_name) + @table_name + end + + def original_table_name #:nodoc: + deprecated_original_property_getter :table_name + end + + # Sets the table name explicitly. Example: + # + # class Project < ActiveRecord::Base + # self.table_name = "project" + # end + # + # You can also just define your own <tt>self.table_name</tt> method; see + # the documentation for ActiveRecord::Base#table_name. + def table_name=(value) + @original_table_name = @table_name if defined?(@table_name) + @table_name = value + @quoted_table_name = nil + @arel_table = nil + @relation = Relation.new(self, arel_table) + end + + def set_table_name(value = nil, &block) #:nodoc: + deprecated_property_setter :table_name, value, block + @quoted_table_name = nil + @arel_table = nil + @relation = Relation.new(self, arel_table) + end + + # Returns a quoted version of the table name, used to construct SQL statements. + def quoted_table_name + @quoted_table_name ||= connection.quote_table_name(table_name) + end + + # Computes the table name, (re)sets it internally, and returns it. + def reset_table_name #:nodoc: + if superclass.abstract_class? + self.table_name = superclass.table_name || compute_table_name + elsif abstract_class? + self.table_name = superclass == Base ? nil : superclass.table_name + else + self.table_name = compute_table_name + end + end + + def full_table_name_prefix #:nodoc: + (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix + end + + # The name of the column containing the object's class when Single Table Inheritance is used + def inheritance_column + if self == Base + 'type' + else + (@inheritance_column ||= nil) || superclass.inheritance_column + end + end + + def original_inheritance_column #:nodoc: + deprecated_original_property_getter :inheritance_column + end + + # Sets the value of inheritance_column + def inheritance_column=(value) + @original_inheritance_column = inheritance_column + @inheritance_column = value.to_s + end + + def set_inheritance_column(value = nil, &block) #:nodoc: + deprecated_property_setter :inheritance_column, value, block + end + + def sequence_name + if base_class == self + @sequence_name ||= reset_sequence_name + else + (@sequence_name ||= nil) || base_class.sequence_name + end + end + + def original_sequence_name #:nodoc: + deprecated_original_property_getter :sequence_name + end + + def reset_sequence_name #:nodoc: + self.sequence_name = connection.default_sequence_name(table_name, primary_key) + end + + # Sets the name of the sequence to use when generating ids to the given + # value, or (if the value is nil or false) to the value returned by the + # given block. This is required for Oracle and is useful for any + # database which relies on sequences for primary key generation. + # + # If a sequence name is not explicitly set when using Oracle or Firebird, + # it will default to the commonly used pattern of: #{table_name}_seq + # + # If a sequence name is not explicitly set when using PostgreSQL, it + # will discover the sequence corresponding to your primary key for you. + # + # class Project < ActiveRecord::Base + # self.sequence_name = "projectseq" # default would have been "project_seq" + # end + def sequence_name=(value) + @original_sequence_name = @sequence_name if defined?(@sequence_name) + @sequence_name = value.to_s + end + + def set_sequence_name(value = nil, &block) #:nodoc: + deprecated_property_setter :sequence_name, value, block + end + + # Indicates whether the table associated with this class exists + def table_exists? + connection.schema_cache.table_exists?(table_name) + end + + # Returns an array of column objects for the table associated with this class. + def columns + @columns ||= connection.schema_cache.columns[table_name].map do |col| + col = col.dup + col.primary = (col.name == primary_key) + col + end + end + + # Returns a hash of column objects for the table associated with this class. + def columns_hash + @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + end + + # Returns a hash where the keys are column names and the values are + # default values when instantiating the AR object for this table. + def column_defaults + @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] + end + + # Returns an array of column names as strings. + def column_names + @column_names ||= columns.map { |column| column.name } + end + + # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", + # and columns used for single table inheritance have been removed. + def content_columns + @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } + end + + # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key + # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute + # is available. + def column_methods_hash #:nodoc: + @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| + attr_name = attr.to_s + methods[attr.to_sym] = attr_name + methods["#{attr}=".to_sym] = attr_name + methods["#{attr}?".to_sym] = attr_name + methods["#{attr}_before_type_cast".to_sym] = attr_name + methods + end + end + + # Resets all the cached information about columns, which will cause them + # to be reloaded on the next request. + # + # The most common usage pattern for this method is probably in a migration, + # when just after creating a table you want to populate it with some default + # values, eg: + # + # class CreateJobLevels < ActiveRecord::Migration + # def up + # create_table :job_levels do |t| + # t.integer :id + # t.string :name + # + # t.timestamps + # end + # + # JobLevel.reset_column_information + # %w{assistant executive manager director}.each do |type| + # JobLevel.create(:name => type) + # end + # end + # + # def down + # drop_table :job_levels + # end + # end + def reset_column_information + connection.clear_cache! + undefine_attribute_methods + connection.schema_cache.clear_table_cache!(table_name) if table_exists? + + @column_names = @content_columns = @column_defaults = @columns = @columns_hash = nil + @dynamic_methods_hash = @inheritance_column = nil + @arel_engine = @relation = nil + end + + def clear_cache! # :nodoc: + connection.schema_cache.clear! + end + + private + + # Guesses the table name, but does not decorate it with prefix and suffix information. + def undecorated_table_name(class_name = base_class.name) + table_name = class_name.to_s.demodulize.underscore + table_name = table_name.pluralize if pluralize_table_names + table_name + end + + # Computes and returns a table name according to default conventions. + def compute_table_name + base = base_class + if self == base + # Nested classes are prefixed with singular parent table name. + if parent < ActiveRecord::Base && !parent.abstract_class? + contained = parent.table_name + contained = contained.singularize if parent.pluralize_table_names + contained += '_' + end + "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" + else + # STI subclasses always use their superclass' table. + base.table_name + end + end + + def deprecated_property_setter(property, value, block) + if block + ActiveSupport::Deprecation.warn( + "Calling set_#{property} is deprecated. If you need to lazily evaluate " \ + "the #{property}, define your own `self.#{property}` class method. You can use `super` " \ + "to get the default #{property} where you would have called `original_#{property}`." + ) + + define_attr_method property, value, false, &block + else + ActiveSupport::Deprecation.warn( + "Calling set_#{property} is deprecated. Please use `self.#{property} = 'the_name'` instead." + ) + + define_attr_method property, value, false + end + end + + def deprecated_original_property_getter(property) + ActiveSupport::Deprecation.warn("original_#{property} is deprecated. Define self.#{property} and call super instead.") + + if !instance_variable_defined?("@original_#{property}") && respond_to?("reset_#{property}") + send("reset_#{property}") + else + instance_variable_get("@original_#{property}") + end + end + end + end +end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb deleted file mode 100644 index 0313abe456..0000000000 --- a/activerecord/lib/active_record/named_scope.rb +++ /dev/null @@ -1,200 +0,0 @@ -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 - - module ClassMethods - # Returns an anonymous \scope. - # - # posts = Post.scoped - # posts.size # Fires "select count(*) from posts" and returns the count - # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects - # - # fruits = Fruit.scoped - # fruits = fruits.where(:color => 'red') if options[:red_only] - # fruits = fruits.limit(10) if limited? - # - # Anonymous \scopes tend to be useful when procedurally generating complex - # queries, where passing intermediate values (\scopes) around as first-class - # objects is convenient. - # - # You can define a \scope that applies to all finders using - # ActiveRecord::Base.default_scope. - def scoped(options = nil) - if options - scoped.apply_finder_options(options) - else - if current_scope - current_scope.clone - else - scope = relation.clone - scope.default_scoped = true - scope - end - end - end - - ## - # Collects attributes from scopes that should be applied when creating - # an AR instance for the particular class this is called on. - def scope_attributes # :nodoc: - if current_scope - current_scope.scope_for_create - else - scope = relation.clone - scope.default_scoped = true - scope.scope_for_create - end - end - - ## - # Are there default attributes associated with this scope? - def scope_attributes? # :nodoc: - current_scope || default_scopes.any? - 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>. - # - # class Shirt < ActiveRecord::Base - # scope :red, where(:color => 'red') - # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) - # end - # - # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, - # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. - # - # Note that this is simply 'syntactic sugar' for defining an actual class method: - # - # class Shirt < ActiveRecord::Base - # def self.red - # where(:color => 'red') - # end - # end - # - # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it - # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, - # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. - # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; - # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> - # all behave as if Shirt.red really was an Array. - # - # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce - # all shirts that are both red and dry clean only. - # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> - # returns the number of garments for which these criteria obtain. Similarly with - # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. - # - # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which - # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If, - # - # class Person < ActiveRecord::Base - # has_many :shirts - # end - # - # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean - # only shirts. - # - # Named \scopes can also be procedural: - # - # class Shirt < ActiveRecord::Base - # scope :colored, lambda { |color| where(:color => color) } - # end - # - # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. - # - # On Ruby 1.9 you can use the 'stabby lambda' syntax: - # - # scope :colored, ->(color) { where(:color => color) } - # - # Note that scopes defined with \scope will be evaluated when they are defined, rather than - # when they are used. For example, the following would be incorrect: - # - # class Post < ActiveRecord::Base - # scope :recent, where('published_at >= ?', Time.now - 1.week) - # end - # - # The example above would be 'frozen' to the <tt>Time.now</tt> value when the <tt>Post</tt> - # class was defined, and so the resultant SQL query would always be the same. The correct - # way to do this would be via a lambda, which will re-evaluate the scope each time - # it is called: - # - # class Post < ActiveRecord::Base - # scope :recent, lambda { where('published_at >= ?', Time.now - 1.week) } - # end - # - # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations: - # - # class Shirt < ActiveRecord::Base - # scope :red, where(:color => 'red') do - # def dom_id - # 'red_shirts' - # end - # end - # end - # - # Scopes can also be used while creating/building a record. - # - # class Article < ActiveRecord::Base - # scope :published, where(:published => true) - # end - # - # Article.published.new.published # => true - # Article.published.create.published # => true - # - # Class methods on your model are automatically available - # on scopes. Assuming the following setup: - # - # class Article < ActiveRecord::Base - # scope :published, where(:published => true) - # scope :featured, where(:featured => true) - # - # def self.latest_article - # order('published_at desc').first - # end - # - # def self.titles - # map(&:title) - # end - # - # end - # - # We are able to call the methods like this: - # - # Article.published.featured.latest_article - # Article.featured.titles - - def scope(name, scope_options = {}) - name = name.to_sym - valid_scope_name?(name) - extension = Module.new(&Proc.new) if block_given? - - scope_proc = lambda do |*args| - options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options - options = scoped.apply_finder_options(options) if options.is_a?(Hash) - - relation = scoped.merge(options) - - extension ? relation.extending(extension) : relation - end - - singleton_class.send(:redefine_method, name, &scope_proc) - end - - protected - - def valid_scope_name?(name) - if respond_to?(name, true) - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." - end - end - end - end -end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index f047a1d9fa..a2fe21043f 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,6 +1,53 @@ +require 'active_support/concern' + module ActiveRecord # = Active Record Persistence module Persistence + extend ActiveSupport::Concern + + module ClassMethods + # Creates an object (or multiple objects) and saves it to the database, if validations pass. + # The resulting object is returned whether the object was saved successfully to the database or not. + # + # The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the + # attributes on the objects that are to be created. + # + # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options + # in the +options+ parameter. + # + # ==== Examples + # # Create a single new object + # User.create(:first_name => 'Jamie') + # + # # Create a single new object using the :admin mass-assignment security role + # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) + # + # # Create a single new object bypassing mass-assignment security + # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) + # + # # Create an Array of new objects + # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) + # + # # Create a single object and pass it into a block to set other attributes. + # User.create(:first_name => 'Jamie') do |u| + # u.is_admin = false + # end + # + # # Creating an Array of new objects using a block, where the block is executed for each object: + # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u| + # u.is_admin = false + # end + def create(attributes = nil, options = {}, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr, options, &block) } + else + object = new(attributes, options, &block) + object.save + object + end + end + end + # 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? diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb new file mode 100644 index 0000000000..09da9ad1d1 --- /dev/null +++ b/activerecord/lib/active_record/querying.rb @@ -0,0 +1,58 @@ +require 'active_support/core_ext/module/delegation' + +module ActiveRecord + module Querying + delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped + delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped + delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped + delegate :find_each, :find_in_batches, :to => :scoped + delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, + :where, :preload, :eager_load, :includes, :from, :lock, :readonly, + :having, :create_with, :uniq, :to => :scoped + delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped + + # Executes a custom SQL query against your database and returns all the results. The results will + # be returned as an array with columns requested encapsulated as attributes of the model you call + # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in + # a Product object with the attributes you specified in the SQL query. + # + # If you call a complicated SQL query which spans multiple tables the columns specified by the + # SELECT will be attributes of the model, whether or not they are columns of the corresponding + # table. + # + # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be + # no database agnostic conversions performed. This should be a last resort because using, for example, + # MySQL specific terms will lock you to using that particular database engine or require you to + # change your call if you switch engines. + # + # ==== Examples + # # A simple SQL query spanning multiple tables + # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id" + # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...] + # + # # 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={"title"=>"The Cheap Man Buys Twice"}>, ...] + def find_by_sql(sql, binds = []) + logging_query_plan do + connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) } + end + end + + # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. + # The use of this method should be restricted to complicated SQL queries that can't be executed + # using the ActiveRecord::Calculations class methods. Look into those before using this. + # + # ==== Parameters + # + # * +sql+ - An SQL statement which should return a count query from the database, see the example below. + # + # ==== Examples + # + # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" + def count_by_sql(sql) + sql = sanitize_conditions(sql) + connection.select_value(sql, "#{name} Count").to_i + end + end +end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 47133e77e8..08cf9fb504 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -22,6 +22,13 @@ module ActiveRecord config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::ConnectionAdapters::ConnectionManagement" + config.action_dispatch.rescue_responses.merge!( + 'ActiveRecord::RecordNotFound' => :not_found, + 'ActiveRecord::StaleObjectError' => :conflict, + 'ActiveRecord::RecordInvalid' => :unprocessable_entity, + 'ActiveRecord::RecordNotSaved' => :unprocessable_entity + ) + rake_tasks do load "active_record/railties/databases.rake" end @@ -78,15 +85,27 @@ module ActiveRecord end end - initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app| - ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.to_cleanup do - ActiveRecord::Base.clear_reloadable_connections! - ActiveRecord::Base.clear_cache! + initializer "active_record.set_reloader_hooks" do |app| + hook = lambda do + ActiveRecord::Base.clear_reloadable_connections! + ActiveRecord::Base.clear_cache! + end + + if app.config.reload_classes_only_on_change + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.to_prepare(&hook) + end + else + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.to_cleanup(&hook) end end end + initializer "active_record.add_watchable_files" do |app| + config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"] + end + config.after_initialize do ActiveSupport.on_load(:active_record) do instantiate_observers diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index abd71793fd..0aafb5184f 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -149,7 +149,9 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false)." task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true - ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) + ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| + ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) + end db_namespace['_dump'].invoke end diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb new file mode 100644 index 0000000000..bf37ab5f14 --- /dev/null +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -0,0 +1,26 @@ +require 'active_support/concern' +require 'active_support/core_ext/class/attribute' + +module ActiveRecord + module ReadonlyAttributes + extend ActiveSupport::Concern + + included do + class_attribute :_attr_readonly, :instance_writer => false + self._attr_readonly = [] + end + + module ClassMethods + # Attributes listed as readonly will be used to create a new record but update operations will + # ignore these fields. + def attr_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 + self._attr_readonly + end + end + end +end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 52968070cb..794a0526fb 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -262,6 +262,10 @@ module ActiveRecord [self] end + def nested? + false + end + # An array of arrays of conditions. Each item in the outside array corresponds to a reflection # in the #chain. The inside arrays are simply conditions (and each condition may itself be # a hash, array, arel predicate, etc...) @@ -457,7 +461,7 @@ module ActiveRecord source_reflection.source_macro end - # A through association is nested iff there would be more than one join table + # A through association is nested if there would be more than one join table def nested? chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 0c32ad5139..ab2882516e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/module/delegation' module ActiveRecord # = Active Record Relation @@ -10,11 +9,7 @@ module ActiveRecord MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order, :uniq] - include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches - - # These are explicitly delegated to improve performance (avoids method_missing) - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a - delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :column_hash,:to => :klass + include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain::ClassMethods, Delegation attr_reader :table, :klass, :loaded attr_accessor :extensions, :default_scoped @@ -137,30 +132,35 @@ module ActiveRecord first || new(attributes, options, &block) end - def respond_to?(method, include_private = false) - arel.respond_to?(method, include_private) || - Array.method_defined?(method) || - @klass.respond_to?(method, include_private) || - super - end - + # Runs EXPLAIN on the query or queries triggered by this relation and + # returns the result as a string. The string is formatted imitating the + # ones printed by the database shell. + # + # Note that this method actually runs the queries, since the results of some + # are needed by the next ones when eager loading is going on. + # + # Please see further details in the + # {Active Record Query Interface guide}[http://edgeguides.rubyonrails.org/active_record_querying.html#running-explain]. def explain - queries = [] - callback = lambda do |*args| - payload = args.last - queries << payload[:sql] unless payload[:exception] || %w(SCHEMA EXPLAIN).include?(payload[:name]) - end + _, queries = collecting_queries_for_explain { exec_queries } + exec_explain(queries) + end - ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do - to_a + def to_a + # We monitor here the entire execution rather than individual SELECTs + # because from the point of view of the user fetching the records of a + # relation is a single unit of work. You want to know if this call takes + # too long, not if the individual queries take too long. + # + # It could be the case that none of the queries involved surpass the + # threshold, and at the same time the sum of them all does. The user + # should get a query plan logged in that case. + logging_query_plan do + exec_queries end - - queries.map do |sql| - "EXPLAIN for: #{sql}\n#{@klass.connection.explain(sql)}" - end.join("\n") end - def to_a + def exec_queries return @records if loaded? default_scoped = with_default_scope @@ -191,6 +191,7 @@ module ActiveRecord @loaded = true @records end + private :exec_queries def as_json(options = nil) #:nodoc: to_a.as_json(options) @@ -236,7 +237,7 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - @klass.send(:with_scope, self, :overwrite) { yield } + @klass.with_scope(self, :overwrite) { yield } end # Updates all records with details given if they match a set of conditions supplied, limits and order can @@ -504,20 +505,6 @@ module ActiveRecord end end - protected - - def method_missing(method, *args, &block) - if Array.method_defined?(method) - to_a.send(method, *args, &block) - elsif @klass.respond_to?(method) - scoping { @klass.send(method, *args, &block) } - elsif arel.respond_to?(method) - arel.send(method, *args, &block) - else - super - end - end - private def references_eager_loaded_tables? @@ -543,6 +530,5 @@ module ActiveRecord # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] end - end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index af86771d2d..0f57e9831d 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -166,6 +166,23 @@ module ActiveRecord 0 end + # This method is designed to perform select by a single column as direct SQL query + # Returns <tt>Array</tt> with values of the specified column name + # The values has same data type as column. + # + # Examples: + # + # Person.pluck(:id) # SELECT people.id FROM people + # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people + # Person.where(:confirmed => true).limit(5).pluck(:id) + # + def pluck(column_name) + scope = self.select(column_name) + self.connection.select_values(scope.to_sql).map! do |value| + type_cast_using_column(value, column_for(column_name)) + end + end + private def perform_calculation(operation, column_name, options = {}) diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb new file mode 100644 index 0000000000..f5fdf437bf --- /dev/null +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -0,0 +1,49 @@ +require 'active_support/core_ext/module/delegation' + +module ActiveRecord + module Delegation + # Set up common delegations for performance (avoids method_missing) + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a + delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, + :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass + + def self.delegate_to_scoped_klass(method) + if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/ + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + scoping { @klass.#{method}(*args, &block) } + end + RUBY + else + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + scoping { @klass.send(#{method.inspect}, *args, &block) } + end + RUBY + end + end + + def respond_to?(method, include_private = false) + super || Array.method_defined?(method) || + @klass.respond_to?(method, include_private) || + arel.respond_to?(method, include_private) + end + + protected + + def method_missing(method, *args, &block) + if Array.method_defined?(method) + ::ActiveRecord::Delegation.delegate method, :to => :to_a + to_a.send(method, *args, &block) + elsif @klass.respond_to?(method) + ::ActiveRecord::Delegation.delegate_to_scoped_klass(method) + scoping { @klass.send(method, *args, &block) } + elsif arel.respond_to?(method) + ::ActiveRecord::Delegation.delegate method, :to => :arel + arel.send(method, *args, &block) + else + super + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb new file mode 100644 index 0000000000..2d7d83d160 --- /dev/null +++ b/activerecord/lib/active_record/sanitization.rb @@ -0,0 +1,194 @@ +require 'active_support/concern' + +module ActiveRecord + module Sanitization + extend ActiveSupport::Concern + + module ClassMethods + def quote_value(value, column = nil) #:nodoc: + connection.quote(value,column) + end + + # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. + def sanitize(object) #:nodoc: + connection.quote(object) + end + + protected + + # Accepts an array, hash, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a WHERE clause. + # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" + # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'" + # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" + def sanitize_sql_for_conditions(condition, table_name = self.table_name) + return nil if condition.blank? + + case condition + when Array; sanitize_sql_array(condition) + when Hash; sanitize_sql_hash_for_conditions(condition, table_name) + else condition + end + end + alias_method :sanitize_sql, :sanitize_sql_for_conditions + + # Accepts an array, hash, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a SET clause. + # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'" + def sanitize_sql_for_assignment(assignments) + case assignments + when Array; sanitize_sql_array(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments) + else assignments + end + end + + # Accepts a hash of SQL conditions and replaces those attributes + # that correspond to a +composed_of+ relationship with their expanded + # aggregate attribute values. + # Given: + # class Person < ActiveRecord::Base + # composed_of :address, :class_name => "Address", + # :mapping => [%w(address_street street), %w(address_city city)] + # end + # Then: + # { :address => Address.new("813 abc st.", "chicago") } + # # => { :address_street => "813 abc st.", :address_city => "chicago" } + def expand_hash_conditions_for_aggregates(attrs) + expanded_attrs = {} + attrs.each do |attr, value| + unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil? + mapping = aggregate_mapping(aggregation) + mapping.each do |field_attr, aggregate_attr| + if mapping.size == 1 && !value.respond_to?(aggregate_attr) + expanded_attrs[field_attr] = value + else + expanded_attrs[field_attr] = value.send(aggregate_attr) + end + end + else + expanded_attrs[attr] = value + end + end + expanded_attrs + end + + # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. + # { :name => "foo'bar", :group_id => 4 } + # # => "name='foo''bar' and group_id= 4" + # { :status => nil, :group_id => [1,2,3] } + # # => "status IS NULL and group_id IN (1,2,3)" + # { :age => 13..18 } + # # => "age BETWEEN 13 AND 18" + # { 'other_records.id' => 7 } + # # => "`other_records`.`id` = 7" + # { :other_records => { :id => 7 } } + # # => "`other_records`.`id` = 7" + # And for value objects on a composed_of relationship: + # { :address => Address.new("123 abc st.", "chicago") } + # # => "address_street='123 abc st.' and address_city='chicago'" + def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) + attrs = expand_hash_conditions_for_aggregates(attrs) + + table = Arel::Table.new(table_name).alias(default_table_name) + PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| + connection.visitor.accept b + }.join(' AND ') + end + alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions + + # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. + # { :status => nil, :group_id => 1 } + # # => "status = NULL , group_id = 1" + def sanitize_sql_hash_for_assignment(attrs) + attrs.map do |attr, value| + "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}" + end.join(', ') + end + + # Accepts an array of conditions. The array has each value + # sanitized and interpolated into the SQL statement. + # ["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) && statement =~ /:\w+/ + replace_named_bind_variables(statement, values.first) + elsif statement.include?('?') + replace_bind_variables(statement, values) + elsif statement.blank? + statement + else + statement % values.collect { |value| connection.quote_string(value.to_s) } + end + end + + alias_method :sanitize_conditions, :sanitize_sql + + def replace_bind_variables(statement, values) #:nodoc: + raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) + bound = values.dup + c = connection + statement.gsub('?') { quote_bound_value(bound.shift, c) } + end + + def replace_named_bind_variables(statement, bind_vars) #:nodoc: + statement.gsub(/(:?):([a-zA-Z]\w*)/) do + if $1 == ':' # skip postgresql casts + $& # return the whole match + elsif bind_vars.include?(match = $2.to_sym) + quote_bound_value(bind_vars[match]) + else + raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" + end + end + end + + def expand_range_bind_variables(bind_vars) #:nodoc: + expanded = [] + + bind_vars.each do |var| + next if var.is_a?(Hash) + + if var.is_a?(Range) + expanded << var.first + expanded << var.last + else + expanded << var + end + end + + expanded + end + + def quote_bound_value(value, c = connection) #:nodoc: + if value.respond_to?(:map) && !value.acts_like?(:string) + if value.respond_to?(:empty?) && value.empty? + c.quote(nil) + else + value.map { |v| c.quote(v) }.join(',') + end + else + c.quote(value) + end + end + + def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc: + unless expected == provided + raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" + end + end + end + + # TODO: Deprecate this + def quoted_id #:nodoc: + quote_value(id, column_for_attribute(self.class.primary_key)) + end + + private + + # Quote strings appropriately for SQL statements. + def quote_value(value, column = nil) + self.class.connection.quote(value, column) + end + end +end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb new file mode 100644 index 0000000000..a8f5e96190 --- /dev/null +++ b/activerecord/lib/active_record/scoping.rb @@ -0,0 +1,152 @@ +require 'active_support/concern' + +module ActiveRecord + module Scoping + extend ActiveSupport::Concern + + included do + include Default + include Named + end + + module ClassMethods + # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be + # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while + # <tt>:create</tt> parameters are an attributes hash. + # + # class Article < ActiveRecord::Base + # def self.create_with_scope + # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do + # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 + # a = create(1) + # a.blog_id # => 1 + # end + # end + # end + # + # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of + # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged. + # + # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing + # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the + # array of strings format for your joins. + # + # class Article < ActiveRecord::Base + # def self.find_with_scope + # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do + # with_scope(:find => limit(10)) do + # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 + # end + # with_scope(:find => where(:author_id => 3)) do + # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 + # end + # end + # end + # end + # + # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method. + # + # class Article < ActiveRecord::Base + # def self.find_with_exclusive_scope + # with_scope(:find => where(:blog_id => 1).limit(1)) do + # with_exclusive_scope(:find => limit(10)) do + # all # => SELECT * from articles LIMIT 10 + # end + # end + # end + # end + # + # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. + def with_scope(scope = {}, action = :merge, &block) + # If another Active Record class has been passed in, get its current scope + scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) + + previous_scope = self.current_scope + + if scope.is_a?(Hash) + # Dup first and second level of hash (method and params). + scope = scope.dup + scope.each do |method, params| + scope[method] = params.dup unless params == true + end + + scope.assert_valid_keys([ :find, :create ]) + relation = construct_finder_arel(scope[:find] || {}) + relation.default_scoped = true unless action == :overwrite + + if previous_scope && previous_scope.create_with_value && scope[:create] + scope_for_create = if action == :merge + previous_scope.create_with_value.merge(scope[:create]) + else + scope[:create] + end + + relation = relation.create_with(scope_for_create) + else + scope_for_create = scope[:create] + scope_for_create ||= previous_scope.create_with_value if previous_scope + relation = relation.create_with(scope_for_create) if scope_for_create + end + + scope = relation + end + + scope = previous_scope.merge(scope) if previous_scope && action == :merge + + self.current_scope = scope + begin + yield + ensure + self.current_scope = previous_scope + end + end + + protected + + # Works like with_scope, but discards any nested properties. + def with_exclusive_scope(method_scoping = {}, &block) + if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) } + raise ArgumentError, <<-MSG + New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope: + + User.unscoped.where(:active => true) + + Or call unscoped with a block: + + User.unscoped do + User.where(:active => true).all + end + + MSG + end + with_scope(method_scoping, :overwrite, &block) + end + + def current_scope #:nodoc: + Thread.current["#{self}_current_scope"] + end + + def current_scope=(scope) #:nodoc: + Thread.current["#{self}_current_scope"] = scope + end + + private + + def construct_finder_arel(options = {}, scope = nil) + relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options + relation = scope.merge(relation) if scope + relation + end + + end + + def populate_with_current_scope_attributes + return unless self.class.scope_attributes? + + self.class.scope_attributes.each do |att,value| + send("#{att}=", value) if respond_to?("#{att}=") + end + end + + end +end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb new file mode 100644 index 0000000000..9840cbccae --- /dev/null +++ b/activerecord/lib/active_record/scoping/default.rb @@ -0,0 +1,140 @@ +require 'active_support/concern' + +module ActiveRecord + module Scoping + module Default + extend ActiveSupport::Concern + + included do + # Stores the default scope for the class + class_attribute :default_scopes, :instance_writer => false + self.default_scopes = [] + end + + module ClassMethods + # Returns a scope for this class without taking into account the default_scope. + # + # class Post < ActiveRecord::Base + # def self.default_scope + # where :published => true + # end + # end + # + # Post.all # Fires "SELECT * FROM posts WHERE published = true" + # Post.unscoped.all # Fires "SELECT * FROM posts" + # + # This method also accepts a block meaning that all queries inside the block will + # not use the default_scope: + # + # Post.unscoped { + # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" + # } + # + # It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt> + # does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same. + # + # Post.unscoped.published + # Post.published + def unscoped #:nodoc: + block_given? ? relation.scoping { yield } : relation + end + + def before_remove_const #:nodoc: + self.current_scope = nil + end + + protected + + # Use this macro in your model to set a default scope for all operations on + # the model. + # + # class Article < ActiveRecord::Base + # default_scope where(:published => true) + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true + # + # The <tt>default_scope</tt> is also applied while creating/building a record. It is not + # applied while updating a record. + # + # Article.new.published # => true + # Article.create.published # => true + # + # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: + # + # class Article < ActiveRecord::Base + # default_scope { where(:published_at => Time.now - 1.week) } + # end + # + # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> + # macro, and it will be called when building the default scope.) + # + # If you use multiple <tt>default_scope</tt> declarations in your model then they will + # be merged together: + # + # class Article < ActiveRecord::Base + # default_scope where(:published => true) + # default_scope where(:rating => 'G') + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' + # + # This is also the case with inheritance and module includes where the parent or module + # defines a <tt>default_scope</tt> and the child or including class defines a second one. + # + # If you need to do more complex things with a default scope, you can alternatively + # define it as a class method: + # + # class Article < ActiveRecord::Base + # def self.default_scope + # # Should return a scope, you can call 'super' here etc. + # end + # end + def default_scope(scope = {}) + scope = Proc.new if block_given? + self.default_scopes = default_scopes + [scope] + end + + def build_default_scope #:nodoc: + if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods + evaluate_default_scope { default_scope } + elsif default_scopes.any? + evaluate_default_scope do + default_scopes.inject(relation) do |default_scope, scope| + if scope.is_a?(Hash) + default_scope.apply_finder_options(scope) + elsif !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(scope.call) + else + default_scope.merge(scope) + end + end + end + end + end + + def ignore_default_scope? #:nodoc: + Thread.current["#{self}_ignore_default_scope"] + end + + def ignore_default_scope=(ignore) #:nodoc: + Thread.current["#{self}_ignore_default_scope"] = ignore + end + + # The ignore_default_scope flag is used to prevent an infinite recursion situation where + # a default scope references a scope which has a default scope which references a scope... + def evaluate_default_scope + return if ignore_default_scope? + + begin + self.ignore_default_scope = true + yield + ensure + self.ignore_default_scope = false + end + end + + end + end + end +end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb new file mode 100644 index 0000000000..f7512bbf5f --- /dev/null +++ b/activerecord/lib/active_record/scoping/named.rb @@ -0,0 +1,202 @@ +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 Scoping + module Named + extend ActiveSupport::Concern + + module ClassMethods + # Returns an anonymous \scope. + # + # posts = Post.scoped + # posts.size # Fires "select count(*) from posts" and returns the count + # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects + # + # fruits = Fruit.scoped + # fruits = fruits.where(:color => 'red') if options[:red_only] + # fruits = fruits.limit(10) if limited? + # + # Anonymous \scopes tend to be useful when procedurally generating complex + # queries, where passing intermediate values (\scopes) around as first-class + # objects is convenient. + # + # You can define a \scope that applies to all finders using + # ActiveRecord::Base.default_scope. + def scoped(options = nil) + if options + scoped.apply_finder_options(options) + else + if current_scope + current_scope.clone + else + scope = relation.clone + scope.default_scoped = true + scope + end + end + end + + ## + # Collects attributes from scopes that should be applied when creating + # an AR instance for the particular class this is called on. + def scope_attributes # :nodoc: + if current_scope + current_scope.scope_for_create + else + scope = relation.clone + scope.default_scoped = true + scope.scope_for_create + end + end + + ## + # Are there default attributes associated with this scope? + def scope_attributes? # :nodoc: + current_scope || default_scopes.any? + 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>. + # + # class Shirt < ActiveRecord::Base + # scope :red, where(:color => 'red') + # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) + # end + # + # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, + # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. + # + # Note that this is simply 'syntactic sugar' for defining an actual class method: + # + # class Shirt < ActiveRecord::Base + # def self.red + # where(:color => 'red') + # end + # end + # + # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it + # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, + # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. + # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; + # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> + # all behave as if Shirt.red really was an Array. + # + # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce + # all shirts that are both red and dry clean only. + # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> + # returns the number of garments for which these criteria obtain. Similarly with + # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. + # + # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which + # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If, + # + # class Person < ActiveRecord::Base + # has_many :shirts + # end + # + # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean + # only shirts. + # + # Named \scopes can also be procedural: + # + # class Shirt < ActiveRecord::Base + # scope :colored, lambda { |color| where(:color => color) } + # end + # + # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. + # + # On Ruby 1.9 you can use the 'stabby lambda' syntax: + # + # scope :colored, ->(color) { where(:color => color) } + # + # Note that scopes defined with \scope will be evaluated when they are defined, rather than + # when they are used. For example, the following would be incorrect: + # + # class Post < ActiveRecord::Base + # scope :recent, where('published_at >= ?', Time.now - 1.week) + # end + # + # The example above would be 'frozen' to the <tt>Time.now</tt> value when the <tt>Post</tt> + # class was defined, and so the resultant SQL query would always be the same. The correct + # way to do this would be via a lambda, which will re-evaluate the scope each time + # it is called: + # + # class Post < ActiveRecord::Base + # scope :recent, lambda { where('published_at >= ?', Time.now - 1.week) } + # end + # + # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations: + # + # class Shirt < ActiveRecord::Base + # scope :red, where(:color => 'red') do + # def dom_id + # 'red_shirts' + # end + # end + # end + # + # Scopes can also be used while creating/building a record. + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # end + # + # Article.published.new.published # => true + # Article.published.create.published # => true + # + # Class methods on your model are automatically available + # on scopes. Assuming the following setup: + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # scope :featured, where(:featured => true) + # + # def self.latest_article + # order('published_at desc').first + # end + # + # def self.titles + # map(&:title) + # end + # + # end + # + # We are able to call the methods like this: + # + # Article.published.featured.latest_article + # Article.featured.titles + + def scope(name, scope_options = {}) + name = name.to_sym + valid_scope_name?(name) + extension = Module.new(&Proc.new) if block_given? + + scope_proc = lambda do |*args| + options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options + options = scoped.apply_finder_options(options) if options.is_a?(Hash) + + relation = scoped.merge(options) + + extension ? relation.extending(extension) : relation + end + + singleton_class.send(:redefine_method, name, &scope_proc) + end + + protected + + def valid_scope_name?(name) + if respond_to?(name, true) + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index c23514c465..5ad40d8cd9 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -2,7 +2,7 @@ module ActiveRecord #:nodoc: # = Active Record Serialization module Serialization extend ActiveSupport::Concern - include ActiveModel::Serializable::JSON + include ActiveModel::Serializers::JSON def serializable_hash(options = nil) options = options.try(:clone) || {} diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 2da836ef0c..0e7f57aa43 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/hash/conversions' module ActiveRecord #:nodoc: module Serialization - include ActiveModel::Serializable::XML + include ActiveModel::Serializers::Xml # Builds an XML document to represent the model. Some configuration is # available through +options+. However more complicated cases should @@ -176,13 +176,13 @@ module ActiveRecord #:nodoc: end end - class XmlSerializer < ActiveModel::Serializable::XML::Serializer #:nodoc: + class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: def initialize(*args) super options[:except] = Array.wrap(options[:except]) | Array.wrap(@serializable.class.inheritance_column) end - class Attribute < ActiveModel::Serializable::XML::Serializer::Attribute #:nodoc: + class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class type = if klass.serialized_attributes.key?(name) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index ae97a3f3ca..2c70d31b94 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -301,7 +301,7 @@ module ActiveRecord protected # 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 + def remember_transaction_record_state #:nodoc: @_start_transaction_state ||= {} @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) unless @_start_transaction_state.include?(:new_record) @@ -314,7 +314,7 @@ module ActiveRecord end # Clear the new record state and id of a record. - def clear_transaction_record_state #:nodoc + def clear_transaction_record_state #:nodoc: if defined?(@_start_transaction_state) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1 @@ -322,7 +322,7 @@ module ActiveRecord end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. - def restore_transaction_record_state(force = false) #:nodoc + def restore_transaction_record_state(force = false) #:nodoc: if defined?(@_start_transaction_state) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 @@ -341,12 +341,12 @@ module ActiveRecord end # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) #:nodoc + def transaction_record_state(state) #:nodoc: @_start_transaction_state[state] if defined?(@_start_transaction_state) end # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. - def transaction_include_action?(action) #:nodoc + def transaction_include_action?(action) #:nodoc: case action when :create transaction_record_state(:new_record) diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb new file mode 100644 index 0000000000..ddcb5f2a7a --- /dev/null +++ b/activerecord/lib/active_record/translation.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module Translation + include ActiveModel::Translation + + # Set the lookup ancestors for ActiveModel. + 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 + end + + # Set the i18n scope to overwrite ActiveModel. + def i18n_scope #:nodoc: + :activerecord + end + end +end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 7af0352a31..9f072c4c39 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,8 +2,9 @@ module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all? - record.errors.add(attribute, :invalid, options.merge(:value => value)) + if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any? + record.errors.add(attribute, :invalid, options.merge(:value => value)) + end end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index f1023ed7ef..56c359650e 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -1,159 +1,162 @@ require "cases/helper" -class AdapterTest < ActiveRecord::TestCase - def setup - @connection = ActiveRecord::Base.connection - end - - def test_tables - tables = @connection.tables - assert tables.include?("accounts") - assert tables.include?("authors") - assert tables.include?("tasks") - assert tables.include?("topics") - end +module ActiveRecord + class AdapterTest < ActiveRecord::TestCase + def setup + @connection = ActiveRecord::Base.connection + end - def test_table_exists? - assert @connection.table_exists?("accounts") - assert !@connection.table_exists?("nonexistingtable") - end + def test_tables + tables = @connection.tables + assert tables.include?("accounts") + assert tables.include?("authors") + assert tables.include?("tasks") + assert tables.include?("topics") + end - def test_indexes - idx_name = "accounts_idx" - - if @connection.respond_to?(:indexes) - indexes = @connection.indexes("accounts") - assert indexes.empty? - - @connection.add_index :accounts, :firm_id, :name => idx_name - indexes = @connection.indexes("accounts") - assert_equal "accounts", indexes.first.table - # OpenBase does not have the concept of a named index - # Indexes are merely properties of columns. - assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter) - assert !indexes.first.unique - assert_equal ["firm_id"], indexes.first.columns - else - warn "#{@connection.class} does not respond to #indexes" + def test_table_exists? + assert @connection.table_exists?("accounts") + assert !@connection.table_exists?("nonexistingtable") + assert !@connection.table_exists?(nil) end - ensure - @connection.remove_index(:accounts, :name => idx_name) rescue nil - end + def test_indexes + idx_name = "accounts_idx" + + if @connection.respond_to?(:indexes) + indexes = @connection.indexes("accounts") + assert indexes.empty? + + @connection.add_index :accounts, :firm_id, :name => idx_name + indexes = @connection.indexes("accounts") + assert_equal "accounts", indexes.first.table + # OpenBase does not have the concept of a named index + # Indexes are merely properties of columns. + assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter) + assert !indexes.first.unique + assert_equal ["firm_id"], indexes.first.columns + else + warn "#{@connection.class} does not respond to #indexes" + end - def test_current_database - if @connection.respond_to?(:current_database) - assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database + ensure + @connection.remove_index(:accounts, :name => idx_name) rescue nil end - end - if current_adapter?(:MysqlAdapter) - def test_charset - assert_not_nil @connection.charset - assert_not_equal 'character_set_database', @connection.charset - assert_equal @connection.show_variable('character_set_database'), @connection.charset + def test_current_database + if @connection.respond_to?(:current_database) + assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database + end end - def test_collation - assert_not_nil @connection.collation - assert_not_equal 'collation_database', @connection.collation - assert_equal @connection.show_variable('collation_database'), @connection.collation - end + if current_adapter?(:MysqlAdapter) + def test_charset + assert_not_nil @connection.charset + assert_not_equal 'character_set_database', @connection.charset + assert_equal @connection.show_variable('character_set_database'), @connection.charset + end - def test_show_nonexistent_variable_returns_nil - assert_nil @connection.show_variable('foo_bar_baz') - end + def test_collation + assert_not_nil @connection.collation + assert_not_equal 'collation_database', @connection.collation + assert_equal @connection.show_variable('collation_database'), @connection.collation + end - def test_not_specifying_database_name_for_cross_database_selects - begin - assert_nothing_raised do - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database)) + def test_show_nonexistent_variable_returns_nil + assert_nil @connection.show_variable('foo_bar_baz') + end - config = ARTest.connection_config - ActiveRecord::Base.connection.execute( - "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \ - "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses" - ) + def test_not_specifying_database_name_for_cross_database_selects + begin + assert_nothing_raised do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database)) + + config = ARTest.connection_config + ActiveRecord::Base.connection.execute( + "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \ + "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses" + ) + end + ensure + ActiveRecord::Base.establish_connection 'arunit' end - ensure - ActiveRecord::Base.establish_connection 'arunit' end end - end - def test_table_alias - def @connection.test_table_alias_length() 10; end - class << @connection - alias_method :old_table_alias_length, :table_alias_length - alias_method :table_alias_length, :test_table_alias_length - end + def test_table_alias + def @connection.test_table_alias_length() 10; end + class << @connection + alias_method :old_table_alias_length, :table_alias_length + alias_method :table_alias_length, :test_table_alias_length + end - assert_equal 'posts', @connection.table_alias_for('posts') - assert_equal 'posts_comm', @connection.table_alias_for('posts_comments') - assert_equal 'dbo_posts', @connection.table_alias_for('dbo.posts') + assert_equal 'posts', @connection.table_alias_for('posts') + assert_equal 'posts_comm', @connection.table_alias_for('posts_comments') + assert_equal 'dbo_posts', @connection.table_alias_for('dbo.posts') - class << @connection - remove_method :table_alias_length - alias_method :table_alias_length, :old_table_alias_length + class << @connection + remove_method :table_alias_length + alias_method :table_alias_length, :old_table_alias_length + end end - end - # test resetting sequences in odd tables in postgreSQL - if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) - require 'models/movie' - require 'models/subscriber' + # test resetting sequences in odd tables in postgreSQL + if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) + require 'models/movie' + require 'models/subscriber' - def test_reset_empty_table_with_custom_pk - Movie.delete_all - Movie.connection.reset_pk_sequence! 'movies' - assert_equal 1, Movie.create(:name => 'fight club').id - end + def test_reset_empty_table_with_custom_pk + Movie.delete_all + Movie.connection.reset_pk_sequence! 'movies' + assert_equal 1, Movie.create(:name => 'fight club').id + end - if ActiveRecord::Base.connection.adapter_name != "FrontBase" - def test_reset_table_with_non_integer_pk - Subscriber.delete_all - Subscriber.connection.reset_pk_sequence! 'subscribers' - sub = Subscriber.new(:name => 'robert drake') - sub.id = 'bob drake' - assert_nothing_raised { sub.save! } + if ActiveRecord::Base.connection.adapter_name != "FrontBase" + def test_reset_table_with_non_integer_pk + Subscriber.delete_all + Subscriber.connection.reset_pk_sequence! 'subscribers' + sub = Subscriber.new(:name => 'robert drake') + sub.id = 'bob drake' + assert_nothing_raised { sub.save! } + end end end - end - def test_uniqueness_violations_are_translated_to_specific_exception - @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" - assert_raises(ActiveRecord::RecordNotUnique) do + def test_uniqueness_violations_are_translated_to_specific_exception @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" + assert_raises(ActiveRecord::RecordNotUnique) do + @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" + end end - end - def test_foreign_key_violations_are_translated_to_specific_exception - unless @connection.adapter_name == 'SQLite' - assert_raises(ActiveRecord::InvalidForeignKey) do - # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method - if @connection.prefetch_primary_key? - id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" - else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + def test_foreign_key_violations_are_translated_to_specific_exception + unless @connection.adapter_name == 'SQLite' + assert_raises(ActiveRecord::InvalidForeignKey) do + # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method + if @connection.prefetch_primary_key? + id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) + @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" + else + @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + end end end end - end - def test_disable_referential_integrity - assert_nothing_raised do - @connection.disable_referential_integrity do - # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method - if @connection.prefetch_primary_key? - id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" - else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + def test_disable_referential_integrity + assert_nothing_raised do + @connection.disable_referential_integrity do + # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method + if @connection.prefetch_primary_key? + id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) + @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" + else + @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + end + # should deleted created record as otherwise disable_referential_integrity will try to enable contraints after executed block + # and will fail (at least on Oracle) + @connection.execute "DELETE FROM fk_test_has_fk" end - # should deleted created record as otherwise disable_referential_integrity will try to enable contraints after executed block - # and will fail (at least on Oracle) - @connection.execute "DELETE FROM fk_test_has_fk" end end end diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index 1aa034ed53..29f885c6e7 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -14,7 +14,7 @@ module ActiveRecord @db_name = db @omgpost = Class.new(ActiveRecord::Base) do - set_table_name "#{db}.#{table}" + self.table_name = "#{db}.#{table}" def self.name; 'Post'; end end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 49514e1539..d5676bc522 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -14,7 +14,7 @@ module ActiveRecord @db_name = db @omgpost = Class.new(ActiveRecord::Base) do - set_table_name "#{db}.#{table}" + self.table_name = "#{db}.#{table}" def self.name; 'Post'; end end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 467e5d7b86..18670b4177 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -26,23 +26,23 @@ class SchemaTest < ActiveRecord::TestCase PK_TABLE_NAME = 'table_with_pk' class Thing1 < ActiveRecord::Base - set_table_name "test_schema.things" + self.table_name = "test_schema.things" end class Thing2 < ActiveRecord::Base - set_table_name "test_schema2.things" + self.table_name = "test_schema2.things" end class Thing3 < ActiveRecord::Base - set_table_name 'test_schema."things.table"' + self.table_name = 'test_schema."things.table"' end class Thing4 < ActiveRecord::Base - set_table_name 'test_schema."Things"' + self.table_name = 'test_schema."Things"' end class Thing5 < ActiveRecord::Base - set_table_name 'things' + self.table_name = 'things' end def setup @@ -68,11 +68,15 @@ class SchemaTest < ActiveRecord::TestCase end def test_schema_change_with_prepared_stmt + altered = false @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]] @connection.exec_query "alter table developers add column zomg int", 'sql', [] + altered = true @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]] ensure - @connection.exec_query "alter table developers drop column if exists zomg", 'sql', [] + # We are not using DROP COLUMN IF EXISTS because that syntax is only + # supported by pg 9.X + @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered end def test_table_exists? diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 5f08f79171..9e7b08ef34 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -1,3 +1,5 @@ +require 'cases/helper' + class PostgreSQLUtilsTest < ActiveSupport::TestCase include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb index 303ba9245a..66e07b71a0 100644 --- a/activerecord/test/cases/adapters/postgresql/view_test.rb +++ b/activerecord/test/cases/adapters/postgresql/view_test.rb @@ -14,7 +14,7 @@ class ViewTest < ActiveRecord::TestCase ] class ThingView < ActiveRecord::Base - set_table_name 'test_schema.view_things' + self.table_name = 'test_schema.view_things' end def setup 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 d75791cab9..7965bb404c 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 @@ -4,7 +4,7 @@ require 'models/tagging' module Namespaced class Post < ActiveRecord::Base - set_table_name 'posts' + self.table_name = 'posts' has_one :tagging, :as => :taggable, :class_name => 'Tagging' end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index c6e451fc57..1dc71ac4cc 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -252,6 +252,41 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_nested_loading_through_has_one_association + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}) + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_order + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'author_addresses.id') + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_order_on_association + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'authors.id') + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_order_on_nested_association + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'posts.id') + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_conditions + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :conditions => "author_addresses.id > 0") + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_conditions_on_association + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :conditions => "authors.id > 0") + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_conditions_on_nested_association + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :conditions => "posts.id > 0") + assert_equal aa.author.posts.count, aa.author.posts.length + end + def test_eager_association_loading_with_belongs_to_and_foreign_keys pets = Pet.find(:all, :include => :owner) assert_equal 3, pets.length diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 8dc1423375..395b59258d 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -71,6 +71,12 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer) end + def test_proxy_association_after_scoped + post = posts(:welcome) + assert_equal post.association(:comments), post.comments.the_association + assert_equal post.association(:comments), post.comments.scoped.the_association + end + private def extension_name(model) 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 34d90cc395..510411ecb2 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 @@ -23,7 +23,7 @@ require 'models/treaty' require 'active_support/core_ext/string/conversions' class ProjectWithAfterCreateHook < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' has_and_belongs_to_many :developers, :class_name => "DeveloperForProjectWithAfterCreateHook", :join_table => "developers_projects", @@ -39,7 +39,7 @@ class ProjectWithAfterCreateHook < ActiveRecord::Base end class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' has_and_belongs_to_many :projects, :class_name => "ProjectWithAfterCreateHook", :join_table => "developers_projects", @@ -48,7 +48,7 @@ class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base end class ProjectWithSymbolsForKeys < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' has_and_belongs_to_many :developers, :class_name => "DeveloperWithSymbolsForKeys", :join_table => :developers_projects, @@ -57,7 +57,7 @@ class ProjectWithSymbolsForKeys < ActiveRecord::Base end class DeveloperWithSymbolsForKeys < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' has_and_belongs_to_many :projects, :class_name => "ProjectWithSymbolsForKeys", :join_table => :developers_projects, @@ -66,7 +66,7 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base end class DeveloperWithCounterSQL < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' has_and_belongs_to_many :projects, :class_name => "DeveloperWithCounterSQL", :join_table => "developers_projects", @@ -77,7 +77,7 @@ end class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, - :parrots, :pirates, :treasures, :price_estimates, :tags, :taggings + :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings def setup_data_for_habtm_case ActiveRecord::Base.connection.execute('delete from countries_treaties') @@ -445,6 +445,26 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert david.projects(true).empty? end + def test_destroy_associations_destroys_multiple_associations + george = parrots(:george) + assert !george.pirates.empty? + assert !george.treasures.empty? + + assert_no_difference "Pirate.count" do + assert_no_difference "Treasure.count" do + george.destroy_associations + end + end + + join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}") + assert join_records.empty? + assert george.pirates(true).empty? + + join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}") + assert join_records.empty? + assert george.treasures(true).empty? + end + def test_deprecated_push_with_attributes_was_removed jamis = developers(:jamis) assert_raise(NoMethodError) do @@ -805,12 +825,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase # clear cache possibly created by other tests david.projects.reset_column_information - # One query for columns, one for primary key - assert_queries(2) { david.projects.columns; david.projects.columns } + assert_queries(1) { david.projects.columns; david.projects.columns } ## and again to verify that reset_column_information clears the cache correctly david.projects.reset_column_information - assert_queries(2) { david.projects.columns; david.projects.columns } + assert_queries(1) { david.projects.columns; david.projects.columns } end def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 4ce8b85098..995afef796 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -733,7 +733,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}" Post.find(post_id).update_column :type, class_name klass = Object.const_set(class_name, Class.new(ActiveRecord::Base)) - klass.set_table_name 'posts' + klass.table_name = 'posts' klass.send(association, association_name, :as => :taggable, :dependent => dependency) klass.find(post_id) end diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 530f5212a2..f920e09410 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -545,6 +545,15 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert_equal [organizations(:nsa)], organizations end + def test_nested_has_many_through_should_not_be_autosaved + c = Categorization.new + c.author = authors(:david) + c.post_taggings.to_a + assert !c.post_taggings.empty? + c.save + assert !c.post_taggings.empty? + end + private def assert_includes_and_joins_equal(query, expected, association) diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index ffe2993e0f..efe71d1771 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'models/computer' require 'models/developer' require 'models/project' require 'models/company' @@ -273,3 +274,18 @@ class OverridingAssociationsTest < ActiveRecord::TestCase ) end end + +class GeneratedMethodsTest < ActiveRecord::TestCase + fixtures :developers, :computers, :posts, :comments + def test_association_methods_override_attribute_methods_of_same_name + assert_equal(developers(:david), computers(:workstation).developer) + # this next line will fail if the attribute methods module is generated lazily + # after the association methods module is generated + assert_equal(developers(:david), computers(:workstation).developer) + assert_equal(developers(:david).id, computers(:workstation)[:developer]) + end + + def test_model_method_overrides_association_method + assert_equal(comments(:greetings).body, posts(:welcome).first_comment) + end +end diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index e03ed33591..86a240d93c 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -14,8 +14,9 @@ module ActiveRecord def setup @klass = Class.new do + def self.base_class; self; end + include ActiveRecord::AttributeMethods - include ActiveRecord::AttributeMethods::Read def self.column_names %w{ one two three } @@ -33,9 +34,6 @@ module ActiveRecord [name, FakeColumn.new(name)] }] end - - def self.serialized_attributes; {}; end - def self.base_class; self; end end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 9300c57819..39e58559b0 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -53,6 +53,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert Boolean.find(b4.id).attribute_present?(:value) end + def test_caching_nil_primary_key + klass = Class.new(Minimalistic) + klass.expects(:reset_primary_key).returns(nil).once + 2.times { klass.primary_key } + end + def test_attribute_keys_on_new_instance t = Topic.new assert_equal nil, t.title, "The topics table has a title column, so it should be nil" @@ -108,6 +114,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert !topic.respond_to?(:nothingness) end + def test_deprecated_underscore_method + topic = Topic.find(1) + assert_equal topic.title, assert_deprecated { topic._title } + end + def test_respond_to_with_custom_primary_key keyboard = Keyboard.create assert_not_nil keyboard.key_number @@ -542,6 +553,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_setting_time_zone_aware_read_attribute + utc_time = Time.utc(2008, 1, 1) + cst_time = utc_time.in_time_zone("Central Time (US & Canada)") + in_time_zone "Pacific Time (US & Canada)" do + record = @target.create(:written_on => cst_time).reload + assert_equal utc_time, record[:written_on] + assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record[:written_on].time_zone + assert_equal Time.utc(2007, 12, 31, 16), record[:written_on].time + end + end + def test_setting_time_zone_aware_attribute_with_string utc_time = Time.utc(2008, 1, 1) (-11..13).each do |timezone_offset| @@ -561,6 +583,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase record = @target.new record.written_on = ' ' assert_nil record.written_on + assert_nil record[:written_on] end end @@ -675,7 +698,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = Topic.new(:id => 5) topic.id = 5 - topic.method(:id).owner.send(:remove_method, :id) + topic.method(:id).owner.send(:undef_method, :id) assert_deprecated do assert_equal 5, topic.id @@ -684,19 +707,19 @@ class AttributeMethodsTest < ActiveRecord::TestCase Topic.undefine_attribute_methods end + def test_read_attribute_with_nil_should_not_asplode + assert_equal nil, Topic.new.read_attribute(nil) + end + private def cached_columns - @cached_columns ||= (time_related_columns_on_topic + serialized_columns_on_topic).map(&:name) + @cached_columns ||= time_related_columns_on_topic.map(&:name) end def time_related_columns_on_topic Topic.columns.select { |c| c.type.in?([:time, :date, :datetime, :timestamp]) } end - def serialized_columns_on_topic - Topic.columns.select { |c| Topic.serialized_attributes.include?(c.name) } - end - def in_time_zone(zone) old_zone = Time.zone old_tz = ActiveRecord::Base.time_zone_aware_attributes diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 4ad2cdfc7e..4c3f2bda57 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -347,6 +347,17 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test client.save! assert_no_queries { assert_equal apple, client.firm } end + + def test_validation_does_not_validate_stale_association_target + valid_developer = Developer.create!(:name => "Dude", :salary => 50_000) + invalid_developer = Developer.new() + + auditlog = AuditLog.new(:message => "foo") + auditlog.developer = invalid_developer + auditlog.developer_id = valid_developer.id + + assert auditlog.valid? + end end class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 997c9e7e9d..dd0aeada51 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -69,6 +69,15 @@ end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts + def test_generated_methods_modules + modules = Computer.ancestors + assert modules.include?(Computer::GeneratedFeatureMethods) + assert_equal(Computer::GeneratedFeatureMethods, Computer.generated_feature_methods) + assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods), + "generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods") + assert_not_equal Computer.generated_feature_methods, Post.generated_feature_methods + end + def test_column_names_are_escaped conn = ActiveRecord::Base.connection classname = conn.class.name[/[^:]*$/] @@ -561,10 +570,12 @@ class BasicsTest < ActiveRecord::TestCase weird = Weird.create('a$b' => 'value') weird.reload assert_equal 'value', weird.send('a$b') + assert_equal 'value', weird.read_attribute('a$b') weird.update_column('a$b', 'value2') weird.reload assert_equal 'value2', weird.send('a$b') + assert_equal 'value2', weird.read_attribute('a$b') end def test_multiparameter_attributes_on_date @@ -1185,19 +1196,6 @@ class BasicsTest < ActiveRecord::TestCase assert(auto.id > 0) end - def quote_column_name(name) - "<#{name}>" - end - - def test_quote_keys - ar = AutoId.new - source = {"foo" => "bar", "baz" => "quux"} - actual = ar.send(:quote_columns, self, source) - inverted = actual.invert - assert_equal("<foo>", inverted["bar"]) - assert_equal("<baz>", inverted["quux"]) - end - def test_sql_injection_via_find assert_raise(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do Topic.find("123456 OR id > 0") @@ -1401,37 +1399,23 @@ class BasicsTest < ActiveRecord::TestCase assert_equal dev, dev.reload end - def test_define_attr_method_with_value - k = Class.new( ActiveRecord::Base ) - k.send(:define_attr_method, :table_name, "foo") - assert_equal "foo", k.table_name - end - - def test_define_attr_method_with_block - k = Class.new( ActiveRecord::Base ) do - class << self - attr_accessor :foo_key - end - end - k.foo_key = "id" - k.send(:define_attr_method, :foo_key) { "sys_" + original_foo_key } - assert_equal "sys_id", k.foo_key - end - def test_set_table_name_with_value k = Class.new( ActiveRecord::Base ) k.table_name = "foo" assert_equal "foo", k.table_name - k.set_table_name "bar" + + assert_deprecated do + k.set_table_name "bar" + end assert_equal "bar", k.table_name end def test_switching_between_table_name assert_difference("GoodJoke.count") do - Joke.set_table_name "cold_jokes" + Joke.table_name = "cold_jokes" Joke.create - Joke.set_table_name "funny_jokes" + Joke.table_name = "funny_jokes" Joke.create end end @@ -1439,50 +1423,182 @@ class BasicsTest < ActiveRecord::TestCase def test_quoted_table_name_after_set_table_name klass = Class.new(ActiveRecord::Base) - klass.set_table_name "foo" + klass.table_name = "foo" assert_equal "foo", klass.table_name assert_equal klass.connection.quote_table_name("foo"), klass.quoted_table_name - klass.set_table_name "bar" + klass.table_name = "bar" assert_equal "bar", klass.table_name assert_equal klass.connection.quote_table_name("bar"), klass.quoted_table_name end def test_set_table_name_with_block k = Class.new( ActiveRecord::Base ) - k.set_table_name { "ks" } - assert_equal "ks", k.table_name + assert_deprecated do + k.set_table_name "foo" + k.set_table_name do + ActiveSupport::Deprecation.silence { original_table_name } + "ks" + end + end + assert_equal "fooks", k.table_name + end + + def test_set_table_name_with_inheritance + k = Class.new( ActiveRecord::Base ) + def k.name; "Foo"; end + def k.table_name; super + "ks"; end + assert_equal "foosks", k.table_name + end + + def test_original_table_name + k = Class.new(ActiveRecord::Base) + def k.name; "Foo"; end + k.table_name = "bar" + + assert_deprecated do + assert_equal "foos", k.original_table_name + end + + k = Class.new(ActiveRecord::Base) + k.table_name = "omg" + k.table_name = "wtf" + + assert_deprecated do + assert_equal "omg", k.original_table_name + end end def test_set_primary_key_with_value k = Class.new( ActiveRecord::Base ) k.primary_key = "foo" assert_equal "foo", k.primary_key - k.set_primary_key "bar" + + assert_deprecated do + k.set_primary_key "bar" + end assert_equal "bar", k.primary_key end def test_set_primary_key_with_block k = Class.new( ActiveRecord::Base ) k.primary_key = 'id' - k.set_primary_key { "sys_" + original_primary_key } + + assert_deprecated do + k.set_primary_key do + "sys_" + ActiveSupport::Deprecation.silence { original_primary_key } + end + end assert_equal "sys_id", k.primary_key end + def test_original_primary_key + k = Class.new(ActiveRecord::Base) + def k.name; "Foo"; end + k.table_name = "posts" + k.primary_key = "bar" + + assert_deprecated do + assert_equal "id", k.original_primary_key + end + + k = Class.new(ActiveRecord::Base) + k.primary_key = "omg" + k.primary_key = "wtf" + + assert_deprecated do + assert_equal "omg", k.original_primary_key + end + end + def test_set_inheritance_column_with_value k = Class.new( ActiveRecord::Base ) k.inheritance_column = "foo" assert_equal "foo", k.inheritance_column - k.set_inheritance_column "bar" + + assert_deprecated do + k.set_inheritance_column "bar" + end assert_equal "bar", k.inheritance_column end def test_set_inheritance_column_with_block k = Class.new( ActiveRecord::Base ) - k.set_inheritance_column { original_inheritance_column + "_id" } + assert_deprecated do + k.set_inheritance_column do + ActiveSupport::Deprecation.silence { original_inheritance_column } + "_id" + end + end assert_equal "type_id", k.inheritance_column end + def test_original_inheritance_column + k = Class.new(ActiveRecord::Base) + def k.name; "Foo"; end + k.inheritance_column = "omg" + + assert_deprecated do + assert_equal "type", k.original_inheritance_column + end + end + + def test_set_sequence_name_with_value + k = Class.new( ActiveRecord::Base ) + k.sequence_name = "foo" + assert_equal "foo", k.sequence_name + + assert_deprecated do + k.set_sequence_name "bar" + end + assert_equal "bar", k.sequence_name + end + + def test_set_sequence_name_with_block + k = Class.new( ActiveRecord::Base ) + k.table_name = "projects" + orig_name = k.sequence_name + return skip "sequences not supported by db" unless orig_name + + assert_deprecated do + k.set_sequence_name do + ActiveSupport::Deprecation.silence { original_sequence_name } + "_lol" + end + end + assert_equal orig_name + "_lol", k.sequence_name + end + + def test_original_sequence_name + k = Class.new(ActiveRecord::Base) + k.table_name = "projects" + orig_name = k.sequence_name + return skip "sequences not supported by db" unless orig_name + + k = Class.new(ActiveRecord::Base) + k.table_name = "projects" + k.sequence_name = "omg" + + assert_deprecated do + assert_equal orig_name, k.original_sequence_name + end + + k = Class.new(ActiveRecord::Base) + k.table_name = "projects" + k.sequence_name = "omg" + k.sequence_name = "wtf" + assert_deprecated do + assert_equal "omg", k.original_sequence_name + end + end + + def test_sequence_name_with_abstract_class + ak = Class.new(ActiveRecord::Base) + ak.abstract_class = true + k = Class.new(ak) + k.table_name = "projects" + orig_name = k.sequence_name + return skip "sequences not supported by db" unless orig_name + assert_equal k.reset_sequence_name, orig_name + end + def test_count_with_join res = Post.count_by_sql "SELECT COUNT(*) FROM posts LEFT JOIN comments ON posts.id=comments.post_id WHERE posts.#{QUOTED_TYPE} = 'Post'" @@ -1848,9 +1964,9 @@ class BasicsTest < ActiveRecord::TestCase def test_clear_cache! # preheat cache - c1 = Post.columns + c1 = Post.connection.schema_cache.columns['posts'] ActiveRecord::Base.clear_cache! - c2 = Post.columns + c2 = Post.connection.schema_cache.columns['posts'] assert_not_equal c1, c2 end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index c38814713a..5abf3d1af4 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/company' +require "models/contract" require 'models/topic' require 'models/edge' require 'models/club' @@ -446,4 +447,28 @@ class CalculationsTest < ActiveRecord::TestCase distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true] assert_equal distinct_authors_for_approved_count, 2 end + + def test_pluck + assert_equal [1,2,3,4], Topic.order(:id).pluck(:id) + end + + def test_pluck_type_cast + topic = topics(:first) + relation = Topic.where(:id => topic.id) + assert_equal [ topic.approved ], relation.pluck(:approved) + assert_equal [ topic.last_read ], relation.pluck(:last_read) + assert_equal [ topic.written_on ], relation.pluck(:written_on) + + end + + def test_pluck_and_uniq + assert_equal [50, 53, 55, 60], Account.order(:credit_limit).uniq.pluck(:credit_limit) + end + + def test_pluck_in_relation + company = Company.first + contract = company.contracts.create! + assert_equal [contract.id], company.contracts.pluck(:id) + end + end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 7f4d25790b..7690769226 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class CallbackDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' class << self def callback_string(callback_method) @@ -48,7 +48,7 @@ class CallbackDeveloperWithFalseValidation < CallbackDeveloper end class ParentDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' attr_accessor :after_save_called before_validation {|record| record.after_save_called = true} end @@ -58,7 +58,7 @@ class ChildDeveloper < ParentDeveloper end class RecursiveCallbackDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' before_save :on_before_save after_save :on_after_save @@ -79,7 +79,7 @@ class RecursiveCallbackDeveloper < ActiveRecord::Base end class ImmutableDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' validates_inclusion_of :salary, :in => 50000..200000 @@ -98,7 +98,7 @@ class ImmutableDeveloper < ActiveRecord::Base end class ImmutableMethodDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' validates_inclusion_of :salary, :in => 50000..200000 @@ -118,7 +118,7 @@ class ImmutableMethodDeveloper < ActiveRecord::Base end class OnCallbacksDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' before_validation { history << :before_validation } before_validation(:on => :create){ history << :before_validation_on_create } @@ -138,7 +138,7 @@ class OnCallbacksDeveloper < ActiveRecord::Base end class CallbackCancellationDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy diff --git a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb new file mode 100644 index 0000000000..7af9079b48 --- /dev/null +++ b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class AbstractAdapterTest < ActiveRecord::TestCase + attr_reader :adapter + + def setup + @adapter = AbstractAdapter.new nil, nil + end + + def test_in_use? + # FIXME: change to refute in Rails 4.0 / mt + assert !adapter.in_use?, 'adapter is not in use' + assert adapter.lease, 'lease adapter' + assert adapter.in_use?, 'adapter is in use' + end + + def test_lease_twice + assert adapter.lease, 'should lease adapter' + assert !adapter.lease, 'should not lease adapter' + end + + def test_last_use + assert !adapter.last_use + adapter.lease + assert adapter.last_use + end + + def test_expire_mutates_in_use + assert adapter.lease, 'lease adapter' + assert adapter.in_use?, 'adapter is in use' + adapter.expire + assert !adapter.in_use?, 'adapter is in use' + end + + def test_close + pool = ConnectionPool.new(Base::ConnectionSpecification.new({}, nil)) + pool.connections << adapter + adapter.pool = pool + + # Make sure the pool marks the connection in use + assert_equal adapter, pool.connection + assert adapter.in_use? + + # Close should put the adapter back in the pool + adapter.close + assert !adapter.in_use? + + assert_equal adapter, pool.connection + end + end + end +end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index bd0d161838..04d543fea9 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -40,8 +40,6 @@ module ActiveRecord def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove @handler.establish_connection 'north america', Base.connection_pool.spec - assert_not_same @handler.retrieve_connection_pool(@klass), - @handler.retrieve_connection_pool(@subklass) @handler.remove_connection @subklass assert_same @handler.retrieve_connection_pool(@klass), diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 79e842f5e1..42e39d534c 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -6,12 +6,6 @@ module ActiveRecord def setup connection = ActiveRecord::Base.connection @cache = SchemaCache.new connection - - if in_memory_db? - connection.create_table :posts do |t| - t.integer :cololumn - end - end end def test_primary_key @@ -19,16 +13,7 @@ module ActiveRecord end def test_primary_key_for_non_existent_table - assert_equal 'id', @cache.primary_keys['omgponies'] - end - - def test_primary_key_is_set_on_columns - posts_columns = @cache.columns_hash['posts'] - assert posts_columns['id'].primary - - (posts_columns.keys - ['id']).each do |key| - assert !posts_columns[key].primary - end + assert_nil @cache.primary_keys['omgponies'] end def test_caches_columns @@ -41,14 +26,18 @@ module ActiveRecord assert_equal columns_hash, @cache.columns_hash['posts'] end - def test_clearing_column_cache + def test_clearing @cache.columns['posts'] @cache.columns_hash['posts'] + @cache.tables['posts'] + @cache.primary_keys['posts'] @cache.clear! assert_equal 0, @cache.columns.size assert_equal 0, @cache.columns_hash.size + assert_equal 0, @cache.tables.size + assert_equal 0, @cache.primary_keys.size end end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index f554ceef35..a1d1177289 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -25,40 +25,6 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end - class FakeBase < ActiveRecord::Base - def self.establish_connection spec - String === spec ? super : spec - end - end - - def test_url_host_no_db - spec = FakeBase.establish_connection 'postgres://foo?encoding=utf8' - assert_equal({ - :adapter => "postgresql", - :database => "", - :host => "foo", - :encoding => "utf8" }, spec) - end - - def test_url_host_db - spec = FakeBase.establish_connection 'postgres://foo/bar?encoding=utf8' - assert_equal({ - :adapter => "postgresql", - :database => "bar", - :host => "foo", - :encoding => "utf8" }, spec) - end - - def test_url_port - spec = FakeBase.establish_connection 'postgres://foo:123?encoding=utf8' - assert_equal({ - :adapter => "postgresql", - :database => "", - :port => 123, - :host => "foo", - :encoding => "utf8" }, spec) - end - def test_app_delegation manager = ConnectionManagement.new(@app) diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 1550fa5530..d170a13b23 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -26,30 +26,6 @@ module ActiveRecord assert !@pool.active_connection? end - def test_clear_stale_cached_connections! - pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec - - threads = [ - Thread.new { pool.connection }, - Thread.new { pool.connection }] - - threads.map { |t| t.join } - - pool.extend Module.new { - attr_accessor :checkins - def checkin conn - @checkins << conn - conn.object_id - end - } - pool.checkins = [] - - cleared_threads = pool.clear_stale_cached_connections! - assert((cleared_threads - threads.map { |x| x.object_id }).empty?, - "threads should have been removed") - assert_equal pool.checkins.length, threads.length - end - def test_checkout_behaviour pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec connection = pool.connection @@ -70,12 +46,15 @@ module ActiveRecord assert thread_ids.include?(t.object_id) end - pool.connection + assert_deprecated do + pool.connection + end threads.each do |t| thread_ids = pool.instance_variable_get(:@reserved_connections).keys assert !thread_ids.include?(t.object_id) end - end.join() + pool.connection.close + end.join end diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb new file mode 100644 index 0000000000..d4b0f236ee --- /dev/null +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -0,0 +1,41 @@ +require "cases/helper" + +module ActiveRecord + class Base + class ConnectionSpecification + class ResolverTest < ActiveRecord::TestCase + def resolve(spec) + Resolver.new(spec, {}).spec.config + end + + def test_url_host_no_db + spec = resolve 'mysql://foo?encoding=utf8' + assert_equal({ + :adapter => "mysql", + :database => "", + :host => "foo", + :encoding => "utf8" }, spec) + end + + def test_url_host_db + spec = resolve 'mysql://foo/bar?encoding=utf8' + assert_equal({ + :adapter => "mysql", + :database => "bar", + :host => "foo", + :encoding => "utf8" }, spec) + end + + def test_url_port + spec = resolve 'mysql://foo:123?encoding=utf8' + assert_equal({ + :adapter => "mysql", + :database => "", + :port => 123, + :host => "foo", + :encoding => "utf8" }, spec) + end + end + end + end +end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb new file mode 100644 index 0000000000..6ae6f83446 --- /dev/null +++ b/activerecord/test/cases/explain_test.rb @@ -0,0 +1,101 @@ +require 'cases/helper' +require 'models/car' +require 'active_support/core_ext/string/strip' + +if ActiveRecord::Base.connection.supports_explain? + class ExplainTest < ActiveRecord::TestCase + fixtures :cars + + def base + ActiveRecord::Base + end + + def connection + base.connection + end + + def test_logging_query_plan + base.logger.expects(:warn).with do |message| + message.starts_with?('EXPLAIN for:') + end + + with_threshold(0) do + Car.where(:name => 'honda').all + end + end + + def test_collect_queries_for_explain + base.auto_explain_threshold_in_seconds = nil + queries = Thread.current[:available_queries_for_explain] = [] + + with_threshold(0) do + Car.where(:name => 'honda').all + end + + sql, binds = queries[0] + assert_match "SELECT", sql + assert_match "honda", sql + assert_equal [], binds + ensure + Thread.current[:available_queries_for_explain] = nil + end + + def test_collecting_queries_for_explain + result, queries = ActiveRecord::Base.collecting_queries_for_explain do + Car.where(:name => 'honda').all + end + + sql, binds = queries[0] + assert_match "SELECT", sql + assert_match "honda", sql + assert_equal [], binds + assert_equal [cars(:honda)], result + end + + def test_exec_explain_with_no_binds + sqls = %w(foo bar) + binds = [[], []] + queries = sqls.zip(binds) + + connection.stubs(:explain).returns('query plan foo', 'query plan bar') + expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n") + assert_equal expected, base.exec_explain(queries) + end + + def test_exec_explain_with_binds + cols = [Object.new, Object.new] + cols[0].expects(:name).returns('wadus') + cols[1].expects(:name).returns('chaflan') + + sqls = %w(foo bar) + binds = [[[cols[0], 1]], [[cols[1], 2]]] + queries = sqls.zip(binds) + + connection.stubs(:explain).returns("query plan foo\n", "query plan bar\n") + expected = <<-SQL.strip_heredoc + EXPLAIN for: #{sqls[0]} [["wadus", 1]] + query plan foo + + EXPLAIN for: #{sqls[1]} [["chaflan", 2]] + query plan bar + SQL + assert_equal expected, base.exec_explain(queries) + end + + def test_silence_auto_explain + base.expects(:collecting_sqls_for_explain).never + base.logger.expects(:warn).never + base.silence_auto_explain do + with_threshold(0) { Car.all } + end + end + + def with_threshold(threshold) + current_threshold = base.auto_explain_threshold_in_seconds + base.auto_explain_threshold_in_seconds = threshold + yield + ensure + base.auto_explain_threshold_in_seconds = current_threshold + end + end +end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 05c4b15407..4514a26e57 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -1163,7 +1163,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_one_message_with_custom_primary_key - Toy.set_primary_key :name + Toy.primary_key = :name begin Toy.find 'Hello World!' rescue ActiveRecord::RecordNotFound => e diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 7e2dafcd01..99dd74c561 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -179,7 +179,7 @@ class FixturesTest < ActiveRecord::TestCase #sanity check to make sure that this file never exists assert Dir[nonexistent_fixture_path+"*"].empty? - assert_raise(FixturesFileNotFound) do + assert_raise(Errno::ENOENT) do ActiveRecord::Fixtures.new( Account.connection, "companies", 'Company', nonexistent_fixture_path) end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 6735bc521b..ae2c230d15 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -68,14 +68,19 @@ module ActiveRecord cattr_accessor :log self.log = [] + attr_reader :ignore + + def initialize(ignore = self.class.ignored_sql) + @ignore = ignore + end + def call(name, start, finish, message_id, values) sql = values[:sql] # FIXME: this seems bad. we should probably have a better way to indicate # the query was cached - unless 'CACHE' == values[:name] - self.class.log << sql unless self.class.ignored_sql.any? { |r| sql =~ r } - end + return if 'CACHE' == values[:name] || ignore.any? { |x| x =~ sql } + self.class.log << sql end end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index b5d8314541..fab858e09c 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -236,11 +236,11 @@ class InheritanceTest < ActiveRecord::TestCase c.save end [ Company, Firm, Client].each { |klass| klass.reset_column_information } - Company.set_inheritance_column('ruby_type') + Company.inheritance_column = 'ruby_type' end def switch_to_default_inheritance_column [ Company, Firm, Client].each { |klass| klass.reset_column_information } - Company.set_inheritance_column('type') + Company.inheritance_column = 'type' end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index e9bd7f07b6..be7edb858f 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -10,8 +10,8 @@ require 'models/string_key_object' class LockWithoutDefault < ActiveRecord::Base; end class LockWithCustomColumnWithoutDefault < ActiveRecord::Base - set_table_name :lock_without_defaults_cust - set_locking_column :custom_lock_version + self.table_name = :lock_without_defaults_cust + self.locking_column = :custom_lock_version end class ReadonlyFirstNamePerson < Person @@ -226,6 +226,48 @@ class OptimisticLockingTest < ActiveRecord::TestCase end end +class SetLockingColumnTest < ActiveRecord::TestCase + def test_set_set_locking_column_with_value + k = Class.new( ActiveRecord::Base ) + k.locking_column = "foo" + assert_equal "foo", k.locking_column + + assert_deprecated do + k.set_locking_column "bar" + end + assert_equal "bar", k.locking_column + end + + def test_set_locking_column_with_block + k = Class.new( ActiveRecord::Base ) + k.locking_column = 'foo' + + assert_deprecated do + k.set_locking_column do + "lock_" + ActiveSupport::Deprecation.silence { original_locking_column } + end + end + assert_equal "lock_foo", k.locking_column + end + + def test_original_locking_column + k = Class.new(ActiveRecord::Base) + k.locking_column = "bar" + + assert_deprecated do + assert_equal ActiveRecord::Locking::Optimistic::ClassMethods::DEFAULT_LOCKING_COLUMN, k.original_locking_column + end + + k = Class.new(ActiveRecord::Base) + k.locking_column = "omg" + k.locking_column = "wtf" + + assert_deprecated do + assert_equal "omg", k.original_locking_column + end + end +end + class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase fixtures :people, :legacy_things, :references @@ -278,6 +320,8 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase assert_equal true, p1.frozen? assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) } assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) } + ensure + remove_counter_column_from(Person, 'legacy_things_count') end private @@ -289,8 +333,8 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter) end - def remove_counter_column_from(model) - model.connection.remove_column model.table_name, :test_count + def remove_counter_column_from(model, col = :test_count) + model.connection.remove_column model.table_name, col model.reset_column_information end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 9e8475465e..e24a5ca5aa 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -13,6 +13,7 @@ class LogSubscriberTest < ActiveRecord::TestCase @old_logger = ActiveRecord::Base.logger @using_identity_map = ActiveRecord::IdentityMap.enabled? ActiveRecord::IdentityMap.enabled = false + Developer.primary_key super ActiveRecord::LogSubscriber.attach_to(:active_record) end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 3e219f2a49..1e68911ab3 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -6,6 +6,8 @@ require 'models/topic' require 'models/developer' require MIGRATIONS_ROOT + "/valid/2_we_need_reminders" +require MIGRATIONS_ROOT + "/rename/1_we_need_things" +require MIGRATIONS_ROOT + "/rename/2_rename_things" require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers" if ActiveRecord::Base.connection.supports_migrations? @@ -13,6 +15,8 @@ if ActiveRecord::Base.connection.supports_migrations? class Reminder < ActiveRecord::Base; end + class Thing < ActiveRecord::Base; end + class ActiveRecord::Migration class << self attr_accessor :message_count @@ -57,6 +61,11 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Base.connection.initialize_schema_migrations_table ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}" + %w(things awesome_things prefix_things_suffix prefix_awesome_things_suffix).each do |table| + Thing.connection.drop_table(table) rescue nil + end + Thing.reset_column_information + %w(reminders people_reminders prefix_reminders_suffix).each do |table| Reminder.connection.drop_table(table) rescue nil end @@ -1027,7 +1036,7 @@ if ActiveRecord::Base.connection.supports_migrations? t.column :title, :string end person_klass = Class.new(Person) - person_klass.set_table_name 'testings' + person_klass.table_name = 'testings' person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99 person_klass.reset_column_information @@ -1226,6 +1235,24 @@ if ActiveRecord::Base.connection.supports_migrations? assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } end + def test_filtering_migrations + assert !Person.column_methods_hash.include?(:last_name) + assert !Reminder.table_exists? + + name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" } + ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter) + + Person.reset_column_information + assert Person.column_methods_hash.include?(:last_name) + assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + + ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter) + + Person.reset_column_information + assert !Person.column_methods_hash.include?(:last_name) + assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + end + class MockMigration < ActiveRecord::Migration attr_reader :went_up, :went_down def initialize @@ -1337,6 +1364,15 @@ if ActiveRecord::Base.connection.supports_migrations? end end + def test_finds_migrations_in_subdirectories + migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid_with_subdirectories").migrations + + [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i| + assert_equal migrations[i].version, pair.first + assert_equal migrations[i].name, pair.last + end + end + def test_finds_migrations_from_two_directories directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps'] migrations = ActiveRecord::Migrator.new(:up, directories).migrations @@ -1534,6 +1570,28 @@ if ActiveRecord::Base.connection.supports_migrations? Reminder.reset_table_name end + def test_rename_table_with_prefix_and_suffix + assert !Thing.table_exists? + ActiveRecord::Base.table_name_prefix = 'prefix_' + ActiveRecord::Base.table_name_suffix = '_suffix' + Thing.reset_table_name + Thing.reset_sequence_name + WeNeedThings.up + + assert Thing.create("content" => "hello world") + assert_equal "hello world", Thing.find(:first).content + + RenameThings.up + Thing.table_name = "prefix_awesome_things_suffix" + + assert_equal "hello world", Thing.find(:first).content + ensure + ActiveRecord::Base.table_name_prefix = '' + ActiveRecord::Base.table_name_suffix = '' + Thing.reset_table_name + Thing.reset_sequence_name + end + def test_add_drop_table_with_prefix_and_suffix assert !Reminder.table_exists? ActiveRecord::Base.table_name_prefix = 'prefix_' @@ -2068,9 +2126,12 @@ if ActiveRecord::Base.connection.supports_migrations? @existing_migrations = Dir[@migrations_path + "/*.rb"] copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"}) - assert File.exists?(@migrations_path + "/4_people_have_hobbies.rb") - assert File.exists?(@migrations_path + "/5_people_have_descriptions.rb") - assert_equal [@migrations_path + "/4_people_have_hobbies.rb", @migrations_path + "/5_people_have_descriptions.rb"], copied.map(&:filename) + assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") + assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename) + + expected = "# This migration comes from bukkits (originally 1)" + assert_equal expected, IO.readlines(@migrations_path + "/4_people_have_hobbies.bukkits.rb")[0].chomp files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"}) @@ -2089,10 +2150,10 @@ if ActiveRecord::Base.connection.supports_migrations? sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy" sources[:omg] = MIGRATIONS_ROOT + "/to_copy2" ActiveRecord::Migration.copy(@migrations_path, sources) - assert File.exists?(@migrations_path + "/4_people_have_hobbies.rb") - assert File.exists?(@migrations_path + "/5_people_have_descriptions.rb") - assert File.exists?(@migrations_path + "/6_create_articles.rb") - assert File.exists?(@migrations_path + "/7_create_comments.rb") + assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") + assert File.exists?(@migrations_path + "/6_create_articles.omg.rb") + assert File.exists?(@migrations_path + "/7_create_comments.omg.rb") files_count = Dir[@migrations_path + "/*.rb"].length ActiveRecord::Migration.copy(@migrations_path, sources) @@ -2107,10 +2168,10 @@ if ActiveRecord::Base.connection.supports_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") - expected = [@migrations_path + "/20100726101010_people_have_hobbies.rb", - @migrations_path + "/20100726101011_people_have_descriptions.rb"] + assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb", + @migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb"] assert_equal expected, copied.map(&:filename) files_count = Dir[@migrations_path + "/*.rb"].length @@ -2132,10 +2193,10 @@ if ActiveRecord::Base.connection.supports_migrations? 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") - assert File.exists?(@migrations_path + "/20100726101012_create_articles.rb") - assert File.exists?(@migrations_path + "/20100726101013_create_comments.rb") + assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exists?(@migrations_path + "/20100726101012_create_articles.omg.rb") + assert File.exists?(@migrations_path + "/20100726101013_create_comments.omg.rb") assert_equal 4, copied.length files_count = Dir[@migrations_path + "/*.rb"].length @@ -2152,8 +2213,8 @@ if ActiveRecord::Base.connection.supports_migrations? 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") + assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb") files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) @@ -2169,15 +2230,34 @@ if ActiveRecord::Base.connection.supports_migrations? @existing_migrations = Dir[@migrations_path + "/*.rb"] sources = ActiveSupport::OrderedHash.new - sources[:bukkits] = sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" + sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" + sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_name_collision" skipped = [] on_skip = Proc.new { |name, migration| skipped << "#{name} #{migration.name}" } copied = ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip) assert_equal 2, copied.length - assert_equal 2, skipped.length - assert_equal ["bukkits PeopleHaveHobbies", "bukkits PeopleHaveDescriptions"], skipped + assert_equal 1, skipped.length + assert_equal ["omg PeopleHaveHobbies"], skipped + ensure + clear + end + + def test_skip_is_not_called_if_migrations_are_from_the_same_plugin + @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" + @existing_migrations = Dir[@migrations_path + "/*.rb"] + + sources = ActiveSupport::OrderedHash.new + sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" + + skipped = [] + on_skip = Proc.new { |name, migration| skipped << "#{name} #{migration.name}" } + copied = ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip) + ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip) + + assert_equal 2, copied.length + assert_equal 0, skipped.length ensure clear end @@ -2188,8 +2268,8 @@ if ActiveRecord::Base.connection.supports_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 File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") assert_equal 2, copied.length end ensure @@ -2203,8 +2283,8 @@ if ActiveRecord::Base.connection.supports_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 File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") assert_equal 2, copied.length end ensure diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index bd51388e05..e704322b5d 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -85,7 +85,6 @@ class MultipleDbTest < ActiveRecord::TestCase end def test_arel_table_engines - assert_not_equal Entrant.arel_engine, Course.arel_engine assert_equal Entrant.arel_engine, Bird.arel_engine end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 2ae9cb4888..2f28dd4767 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -617,6 +617,11 @@ module NestedAttributesOnACollectionAssociationTests assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name] end + def test_should_take_a_hash_with_owner_attributes_and_assign_the_attributes_to_the_associated_model + @pirate.birds.create :name => 'bird', :pirate_attributes => {:id => @pirate.id.to_s, :catchphrase => 'Holla!'} + assert_equal 'Holla!', @pirate.reload.catchphrase + end + def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}" do @pirate.attributes = { association_getter => [{ :id => 1234567890 }] } diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 4bb5752096..0669707baf 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -23,6 +23,11 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal keyboard.to_key, [keyboard.id] end + def test_read_attribute_with_custom_primary_key + keyboard = Keyboard.create! + assert_equal keyboard.key_number, keyboard.read_attribute(:id) + end + def test_to_key_with_primary_key_after_destroy topic = Topic.find(1) topic.destroy @@ -142,8 +147,38 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal k.connection.quote_column_name("id"), k.quoted_primary_key k.primary_key = "foo" assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key - k.set_primary_key "bar" - assert_equal k.connection.quote_column_name("bar"), k.quoted_primary_key + end + + def test_two_models_with_same_table_but_different_primary_key + k1 = Class.new(ActiveRecord::Base) + k1.table_name = 'posts' + k1.primary_key = 'id' + + k2 = Class.new(ActiveRecord::Base) + k2.table_name = 'posts' + k2.primary_key = 'title' + + assert k1.columns.find { |c| c.name == 'id' }.primary + assert !k1.columns.find { |c| c.name == 'title' }.primary + assert k1.columns_hash['id'].primary + assert !k1.columns_hash['title'].primary + + assert !k2.columns.find { |c| c.name == 'id' }.primary + assert k2.columns.find { |c| c.name == 'title' }.primary + assert !k2.columns_hash['id'].primary + assert k2.columns_hash['title'].primary + end + + def test_models_with_same_table_have_different_columns + k1 = Class.new(ActiveRecord::Base) + k1.table_name = 'posts' + + k2 = Class.new(ActiveRecord::Base) + k2.table_name = 'posts' + + k1.columns.zip(k2.columns).each do |col1, col2| + assert !col1.equal?(col2) + end end end @@ -155,9 +190,8 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase connection = ActiveRecord::Base.remove_connection - model = Class.new(ActiveRecord::Base) do - set_primary_key 'foo' - end + model = Class.new(ActiveRecord::Base) + model.primary_key = 'foo' assert_equal 'foo', model.primary_key diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 1e2093273e..c7f6ab60ef 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -527,7 +527,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_default_scope_is_threadsafe if in_memory_db? - skip "in memory db can't share a db between threads" + return skip "in memory db can't share a db between threads" end threads = [] diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb index 258cee7aba..bcacbb9b5f 100644 --- a/activerecord/test/cases/session_store/session_test.rb +++ b/activerecord/test/cases/session_store/session_test.rb @@ -5,10 +5,11 @@ require 'active_record/session_store' module ActiveRecord class SessionStore class SessionTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? && ActiveRecord::Base.connection.supports_ddl_transactions? + self.use_transactional_fixtures = false def setup super + ActiveRecord::Base.connection.schema_cache.clear! Session.drop_table! if Session.table_exists? end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 074fd39e65..5a3f9a9711 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -24,9 +24,9 @@ class StoreTest < ActiveRecord::TestCase @john.settings[:icecream] = 'graeters' @john.save - assert 'graeters', @john.reload.settings[:icecream] + assert_equal 'graeters', @john.reload.settings[:icecream] end - + test "updating the store will mark it as changed" do @john.color = 'red' assert @john.settings_changed? diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 85f222bca2..f8b3e01a49 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -6,7 +6,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase fixtures :topics class TopicWithCallbacks < ActiveRecord::Base - set_table_name :topics + self.table_name = :topics after_commit{|record| record.send(:do_after_commit, nil)} after_commit(:on => :create){|record| record.send(:do_after_commit, :create)} @@ -252,7 +252,7 @@ class TransactionObserverCallbacksTest < ActiveRecord::TestCase fixtures :topics class TopicWithObserverAttached < ActiveRecord::Base - set_table_name :topics + self.table_name = :topics def history @history ||= [] end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 110a18772f..203dd054f1 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -563,6 +563,7 @@ if current_adapter?(:PostgreSQLAdapter) topic.approved = !topic.approved? topic.save! end + Topic.connection.close end end @@ -598,6 +599,7 @@ if current_adapter?(:PostgreSQLAdapter) dev = Developer.find(1) assert_equal original_salary, dev.salary end + Developer.connection.close end end @@ -610,6 +612,7 @@ if current_adapter?(:PostgreSQLAdapter) assert_equal original_salary, Developer.find(1).salary end end + Developer.connection.close end threads.each { |t| t.join } diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index 56e345990f..768fbc5b0a 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -61,6 +61,16 @@ class AssociationValidationTest < ActiveRecord::TestCase assert r.valid? end + def test_validates_associated_marked_for_destruction + Topic.validates_associated(:replies) + Reply.validates_presence_of(:content) + t = Topic.new + t.replies << Reply.new + assert t.invalid? + t.replies.first.mark_for_destruction + assert t.valid? + end + def test_validates_associated_with_custom_message_using_quotes Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" Topic.validates_presence_of :content diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index c3e494866b..e575a98170 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -8,7 +8,7 @@ require 'models/parrot' require 'models/company' class ProtectedPerson < ActiveRecord::Base - set_table_name 'people' + self.table_name = 'people' attr_accessor :addon attr_protected :first_name end diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 0b54c309d1..5a38f2c6ee 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -18,6 +18,11 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal topic, t end + def test_roundtrip_serialized_column + topic = Topic.new(:content => {:omg=>:lol}) + assert_equal({:omg=>:lol}, YAML.load(YAML.dump(topic)).content) + end + def test_encode_with_coder topic = Topic.first coder = {} diff --git a/activerecord/test/migrations/rename/1_we_need_things.rb b/activerecord/test/migrations/rename/1_we_need_things.rb new file mode 100644 index 0000000000..cdbe0b1679 --- /dev/null +++ b/activerecord/test/migrations/rename/1_we_need_things.rb @@ -0,0 +1,11 @@ +class WeNeedThings < ActiveRecord::Migration + def self.up + create_table("things") do |t| + t.column :content, :text + end + end + + def self.down + drop_table "things" + end +end
\ No newline at end of file diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb new file mode 100644 index 0000000000..d441b71fc9 --- /dev/null +++ b/activerecord/test/migrations/rename/2_rename_things.rb @@ -0,0 +1,9 @@ +class RenameThings < ActiveRecord::Migration + def self.up + rename_table "things", "awesome_things" + end + + def self.down + rename_table "awesome_things", "things" + end +end
\ No newline at end of file diff --git a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb new file mode 100644 index 0000000000..e438cf5999 --- /dev/null +++ b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb @@ -0,0 +1,9 @@ +class PeopleHaveLastNames < ActiveRecord::Migration + def self.up + add_column "people", "hobbies", :string + end + + def self.down + remove_column "people", "hobbies" + end +end diff --git a/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb new file mode 100644 index 0000000000..06cb911117 --- /dev/null +++ b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb @@ -0,0 +1,9 @@ +class ValidPeopleHaveLastNames < ActiveRecord::Migration + def self.up + add_column "people", "last_name", :string + end + + def self.down + remove_column "people", "last_name" + end +end diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb new file mode 100644 index 0000000000..d5e71ce8ef --- /dev/null +++ b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb @@ -0,0 +1,12 @@ +class WeNeedReminders < ActiveRecord::Migration + def self.up + create_table("reminders") do |t| + t.column :content, :text + t.column :remind_at, :datetime + end + end + + def self.down + drop_table "reminders" + end +end
\ No newline at end of file diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb new file mode 100644 index 0000000000..21c9ca5328 --- /dev/null +++ b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb @@ -0,0 +1,12 @@ +class InnocentJointable < ActiveRecord::Migration + def self.up + create_table("people_reminders", :id => false) do |t| + t.column :reminder_id, :integer + t.column :person_id, :integer + end + end + + def self.down + drop_table "people_reminders" + end +end
\ No newline at end of file diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 23db5650d4..bfadfd9d75 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -128,7 +128,6 @@ class Author < ActiveRecord::Base belongs_to :author_address, :dependent => :destroy belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress" - has_many :post_categories, :through => :posts, :source => :categories has_many :category_post_comments, :through => :categories, :source => :post_comments has_many :misc_posts, :class_name => 'Post', diff --git a/activerecord/test/models/bird.rb b/activerecord/test/models/bird.rb index e61d48e6a5..dff099c1fb 100644 --- a/activerecord/test/models/bird.rb +++ b/activerecord/test/models/bird.rb @@ -1,9 +1,12 @@ class Bird < ActiveRecord::Base + belongs_to :pirate validates_presence_of :name + accepts_nested_attributes_for :pirate + attr_accessor :cancel_save_from_callback before_save :cancel_save_callback_method, :if => :cancel_save_from_callback def cancel_save_callback_method false end -end
\ No newline at end of file +end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 78eb4c57ac..fe9c465c81 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -4,7 +4,7 @@ end class Company < AbstractCompany attr_protected :rating - set_sequence_name :companies_nonstd_seq + self.sequence_name = :companies_nonstd_seq validates_presence_of :name diff --git a/activerecord/test/models/country.rb b/activerecord/test/models/country.rb index 15e3a1de0b..7db9a4e731 100644 --- a/activerecord/test/models/country.rb +++ b/activerecord/test/models/country.rb @@ -1,6 +1,6 @@ class Country < ActiveRecord::Base - set_primary_key :country_id + self.primary_key = :country_id has_and_belongs_to_many :treaties diff --git a/activerecord/test/models/dashboard.rb b/activerecord/test/models/dashboard.rb index a8a25834b1..1b3b54545f 100644 --- a/activerecord/test/models/dashboard.rb +++ b/activerecord/test/models/dashboard.rb @@ -1,3 +1,3 @@ class Dashboard < ActiveRecord::Base - set_primary_key :dashboard_id -end
\ No newline at end of file + self.primary_key = :dashboard_id +end diff --git a/activerecord/test/models/joke.rb b/activerecord/test/models/joke.rb index d7f01e59e6..edda4655dc 100644 --- a/activerecord/test/models/joke.rb +++ b/activerecord/test/models/joke.rb @@ -1,7 +1,7 @@ class Joke < ActiveRecord::Base - set_table_name 'funny_jokes' + self.table_name = 'funny_jokes' end class GoodJoke < ActiveRecord::Base - set_table_name 'funny_jokes' + self.table_name = 'funny_jokes' end diff --git a/activerecord/test/models/keyboard.rb b/activerecord/test/models/keyboard.rb index 32a4a7fad0..39347e274e 100644 --- a/activerecord/test/models/keyboard.rb +++ b/activerecord/test/models/keyboard.rb @@ -1,3 +1,3 @@ class Keyboard < ActiveRecord::Base - set_primary_key 'key_number' + self.primary_key = 'key_number' end diff --git a/activerecord/test/models/legacy_thing.rb b/activerecord/test/models/legacy_thing.rb index eaeb642d12..eead181a0e 100644 --- a/activerecord/test/models/legacy_thing.rb +++ b/activerecord/test/models/legacy_thing.rb @@ -1,3 +1,3 @@ class LegacyThing < ActiveRecord::Base - set_locking_column :version + self.locking_column = :version end diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb index b96c054f6c..3fcd5e4b69 100644 --- a/activerecord/test/models/liquid.rb +++ b/activerecord/test/models/liquid.rb @@ -1,5 +1,5 @@ class Liquid < ActiveRecord::Base - set_table_name :liquid + self.table_name = :liquid has_many :molecules, :uniq => true end diff --git a/activerecord/test/models/minivan.rb b/activerecord/test/models/minivan.rb index 830cdb5796..4fe79720ad 100644 --- a/activerecord/test/models/minivan.rb +++ b/activerecord/test/models/minivan.rb @@ -1,5 +1,5 @@ class Minivan < ActiveRecord::Base - set_primary_key :minivan_id + self.primary_key = :minivan_id belongs_to :speedometer has_one :dashboard, :through => :speedometer diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb index 853f2682b3..763baefd91 100644 --- a/activerecord/test/models/mixed_case_monkey.rb +++ b/activerecord/test/models/mixed_case_monkey.rb @@ -1,3 +1,3 @@ class MixedCaseMonkey < ActiveRecord::Base - set_primary_key 'monkeyID' + self.primary_key = 'monkeyID' end diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index 5760b991ec..fea55f4535 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -1,5 +1,5 @@ class Owner < ActiveRecord::Base - set_primary_key :owner_id + self.primary_key = :owner_id has_many :pets has_many :toys, :through => :pets end diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index 737ef9131b..c4ee2bd19d 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -1,5 +1,6 @@ class Parrot < ActiveRecord::Base - set_inheritance_column :parrot_sti_class + self.inheritance_column = :parrot_sti_class + has_and_belongs_to_many :pirates has_and_belongs_to_many :treasures has_many :loots, :as => :looter diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index 113826756a..3cd5bceed5 100644 --- a/activerecord/test/models/pet.rb +++ b/activerecord/test/models/pet.rb @@ -2,7 +2,7 @@ class Pet < ActiveRecord::Base attr_accessor :current_user - set_primary_key :pet_id + self.primary_key = :pet_id belongs_to :owner, :touch => true has_many :toys diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 198a963cbc..1cab78d8c7 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -24,6 +24,10 @@ class Post < ActiveRecord::Base belongs_to :author_with_posts, :class_name => "Author", :foreign_key => :author_id, :include => :posts belongs_to :author_with_address, :class_name => "Author", :foreign_key => :author_id, :include => :author_address + def first_comment + super.body + end + has_one :first_comment, :class_name => 'Comment', :order => 'id ASC' has_one :last_comment, :class_name => 'Comment', :order => 'id desc' scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} } @@ -40,6 +44,10 @@ class Post < ActiveRecord::Base def newest created.last end + + def the_association + proxy_association + end end has_many :author_favorites, :through => :author @@ -181,4 +189,4 @@ end class SpecialPostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' default_scope where(:id => [1, 5,6]) -end
\ No newline at end of file +end diff --git a/activerecord/test/models/speedometer.rb b/activerecord/test/models/speedometer.rb index 94743eff8e..0a7d38d8ec 100644 --- a/activerecord/test/models/speedometer.rb +++ b/activerecord/test/models/speedometer.rb @@ -1,4 +1,4 @@ class Speedometer < ActiveRecord::Base - set_primary_key :speedometer_id + self.primary_key = :speedometer_id belongs_to :dashboard -end
\ No newline at end of file +end diff --git a/activerecord/test/models/string_key_object.rb b/activerecord/test/models/string_key_object.rb index f8d4c6e0e4..f084ec1bdc 100644 --- a/activerecord/test/models/string_key_object.rb +++ b/activerecord/test/models/string_key_object.rb @@ -1,3 +1,3 @@ class StringKeyObject < ActiveRecord::Base - set_primary_key :id + self.primary_key = :id end diff --git a/activerecord/test/models/subscriber.rb b/activerecord/test/models/subscriber.rb index 5b78014e6f..76e85a0cd3 100644 --- a/activerecord/test/models/subscriber.rb +++ b/activerecord/test/models/subscriber.rb @@ -1,5 +1,5 @@ class Subscriber < ActiveRecord::Base - set_primary_key 'nick' + self.primary_key = 'nick' has_many :subscriptions has_many :books, :through => :subscriptions end diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb index 6c45e99671..0377e50011 100644 --- a/activerecord/test/models/toy.rb +++ b/activerecord/test/models/toy.rb @@ -1,5 +1,5 @@ class Toy < ActiveRecord::Base - set_primary_key :toy_id + self.primary_key = :toy_id belongs_to :pet scope :with_pet, joins(:pet) diff --git a/activerecord/test/models/treaty.rb b/activerecord/test/models/treaty.rb index b46537f0d2..41fd1350f3 100644 --- a/activerecord/test/models/treaty.rb +++ b/activerecord/test/models/treaty.rb @@ -1,6 +1,6 @@ class Treaty < ActiveRecord::Base - set_primary_key :treaty_id + self.primary_key = :treaty_id has_and_belongs_to_many :countries diff --git a/activerecord/test/models/warehouse_thing.rb b/activerecord/test/models/warehouse_thing.rb index 6ace1183cc..f20bd1a245 100644 --- a/activerecord/test/models/warehouse_thing.rb +++ b/activerecord/test/models/warehouse_thing.rb @@ -1,5 +1,5 @@ class WarehouseThing < ActiveRecord::Base - set_table_name "warehouse-things" + self.table_name = "warehouse-things" validates_uniqueness_of :value -end
\ No newline at end of file +end diff --git a/activeresource/lib/active_resource/custom_methods.rb b/activeresource/lib/active_resource/custom_methods.rb index f7cb381711..2a651dd48e 100644 --- a/activeresource/lib/active_resource/custom_methods.rb +++ b/activeresource/lib/active_resource/custom_methods.rb @@ -85,37 +85,35 @@ module ActiveResource end end - module InstanceMethods - def get(method_name, options = {}) - self.class.format.decode(connection.get(custom_method_element_url(method_name, options), self.class.headers).body) - end + def get(method_name, options = {}) + self.class.format.decode(connection.get(custom_method_element_url(method_name, options), self.class.headers).body) + end - def post(method_name, options = {}, body = nil) - request_body = body.blank? ? encode : body - if new? - connection.post(custom_method_new_element_url(method_name, options), request_body, self.class.headers) - else - connection.post(custom_method_element_url(method_name, options), request_body, self.class.headers) - end + def post(method_name, options = {}, body = nil) + request_body = body.blank? ? encode : body + if new? + connection.post(custom_method_new_element_url(method_name, options), request_body, self.class.headers) + else + connection.post(custom_method_element_url(method_name, options), request_body, self.class.headers) end + end - def put(method_name, options = {}, body = '') - connection.put(custom_method_element_url(method_name, options), body, self.class.headers) - end + def put(method_name, options = {}, body = '') + connection.put(custom_method_element_url(method_name, options), body, self.class.headers) + end - def delete(method_name, options = {}) - connection.delete(custom_method_element_url(method_name, options), self.class.headers) - end + def delete(method_name, options = {}) + connection.delete(custom_method_element_url(method_name, options), self.class.headers) + end - private - def custom_method_element_url(method_name, options = {}) - "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}" - end + private + def custom_method_element_url(method_name, options = {}) + "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}" + end - def custom_method_new_element_url(method_name, options = {}) - "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}" - end - end + def custom_method_new_element_url(method_name, options = {}) + "#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}" + end end end diff --git a/activeresource/lib/active_resource/exceptions.rb b/activeresource/lib/active_resource/exceptions.rb index 6b953b28ad..51bede3bd0 100644 --- a/activeresource/lib/active_resource/exceptions.rb +++ b/activeresource/lib/active_resource/exceptions.rb @@ -33,35 +33,45 @@ module ActiveResource # 3xx Redirection class Redirection < ConnectionError # :nodoc: - def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end + def to_s + response['Location'] ? "#{super} => #{response['Location']}" : super + end end - # Raised when ... - class MissingPrefixParam < ArgumentError; end # :nodoc: + class MissingPrefixParam < ArgumentError # :nodoc: + end # 4xx Client Error - class ClientError < ConnectionError; end # :nodoc: + class ClientError < ConnectionError # :nodoc: + end # 400 Bad Request - class BadRequest < ClientError; end # :nodoc + class BadRequest < ClientError # :nodoc: + end # 401 Unauthorized - class UnauthorizedAccess < ClientError; end # :nodoc + class UnauthorizedAccess < ClientError # :nodoc: + end # 403 Forbidden - class ForbiddenAccess < ClientError; end # :nodoc + class ForbiddenAccess < ClientError # :nodoc: + end # 404 Not Found - class ResourceNotFound < ClientError; end # :nodoc: + class ResourceNotFound < ClientError # :nodoc: + end # 409 Conflict - class ResourceConflict < ClientError; end # :nodoc: + class ResourceConflict < ClientError # :nodoc: + end # 410 Gone - class ResourceGone < ClientError; end # :nodoc: + class ResourceGone < ClientError # :nodoc: + end # 5xx Server Error - class ServerError < ConnectionError; end # :nodoc: + class ServerError < ConnectionError # :nodoc: + end # 405 Method Not Allowed class MethodNotAllowed < ClientError # :nodoc: diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 465b0c1e16..1516472fed 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,10 @@ ## Rails 3.2.0 (unreleased) ## +* Added Enumerable#pluck to wrap the common pattern of collect(&:method) *DHH* + +* Module#synchronize is deprecated with no replacement. Please use `monitor` + from ruby's standard library. + * (Date|DateTime|Time)#beginning_of_week accept an optional argument to be able to set the day at which weeks are assumed to start. @@ -42,6 +47,28 @@ * ActiveSupport::OrderedHash now has different behavior for #each and \#each_pair when given a block accepting its parameters with a splat. *Andrew Radev* +* ActiveSupport::BufferedLogger#silence is deprecated. If you want to squelch + logs for a certain block, change the log level for that block. + +* ActiveSupport::BufferedLogger#open_log is deprecated. This method should + not have been public in the first place. + +* ActiveSupport::BufferedLogger's behavior of automatically creating the + directory for your log file is deprecated. Please make sure to create the + directory for your log file before instantiating. + +* ActiveSupport::BufferedLogger#auto_flushing is deprecated. Either set the + sync level on the underlying file handle like this: + + f = File.open('foo.log', 'w') + f.sync = true + ActiveSupport::BufferedLogger.new f + + Or tune your filesystem. The FS cache is now what controls flushing. + +* ActiveSupport::BufferedLogger#flush is deprecated. Set sync on your + filehandle, or tune your filesystem. + ## Rails 3.1.0 (August 30, 2011) ## * ActiveSupport::Dependencies#load and ActiveSupport::Dependencies#require now diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb index 136e245859..775f9928e6 100644 --- a/activesupport/lib/active_support/buffered_logger.rb +++ b/activesupport/lib/active_support/buffered_logger.rb @@ -1,5 +1,9 @@ require 'thread' +require 'logger' +require 'active_support/core_ext/logger' require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/deprecation' +require 'fileutils' module ActiveSupport # Inspired by the buffered logger idea by Ezra @@ -25,40 +29,35 @@ module ActiveSupport # Silences the logger for the duration of the block. def silence(temporary_level = ERROR) if silencer - old_logger_level = @tmp_levels[Thread.current] begin - @tmp_levels[Thread.current] = temporary_level - yield self + logger = self.class.new @log_dest, temporary_level + yield logger ensure - if old_logger_level - @tmp_levels[Thread.current] = old_logger_level - else - @tmp_levels.delete(Thread.current) - end + logger.close end else yield self end end + deprecate :silence - attr_writer :level attr_reader :auto_flushing + deprecate :auto_flushing def initialize(log, level = DEBUG) @level = level - @tmp_levels = {} - @buffer = Hash.new { |h,k| h[k] = [] } - @auto_flushing = 1 - @guard = Mutex.new - - if log.respond_to?(:write) - @log = log - elsif File.exist?(log) - @log = open_log(log, (File::WRONLY | File::APPEND)) - else - FileUtils.mkdir_p(File.dirname(log)) - @log = open_log(log, (File::WRONLY | File::APPEND | File::CREAT)) + @log_dest = log + + unless log.respond_to?(:write) + unless File.exist?(File.dirname(log)) + ActiveSupport::Deprecation.warn(<<-eowarn) +Automatic directory creation for '#{log}' is deprecated. Please make sure the directory for your log file exists before creating the logger. + eowarn + FileUtils.mkdir_p(File.dirname(log)) + end end + + @log = open_logfile log end def open_log(log, mode) @@ -67,20 +66,18 @@ module ActiveSupport open_log.sync = true end end + deprecate :open_log def level - @tmp_levels[Thread.current] || @level + @log.level + end + + def level=(l) + @log.level = l end def add(severity, message = nil, progname = nil, &block) - return if level > severity - message = (message || (block && block.call) || progname).to_s - # If a newline is necessary then create a new message ending with a newline. - # Ensures that the original message is not mutated. - message = "#{message}\n" unless message[-1] == ?\n - buffer << message - auto_flush - message + @log.add(severity, message, progname, &block) end # Dynamically add methods such as: @@ -104,62 +101,25 @@ module ActiveSupport # never auto-flush. If you turn auto-flushing off, be sure to regularly # flush the log yourself -- it will eat up memory until you do. def auto_flushing=(period) - @auto_flushing = - case period - when true; 1 - when false, nil, 0; MAX_BUFFER_SIZE - when Integer; period - else raise ArgumentError, "Unrecognized auto_flushing period: #{period.inspect}" - end end + deprecate :auto_flushing= def flush - @guard.synchronize do - write_buffer(buffer) - - # Important to do this even if buffer was empty or else @buffer will - # accumulate empty arrays for each request where nothing was logged. - clear_buffer + end + deprecate :flush - # Clear buffers associated with dead threads or else spawned threads - # that don't call flush will result in a memory leak. - flush_dead_buffers - end + def respond_to?(method, include_private = false) + return false if method.to_s == "flush" + super end def close - flush - @log.close if @log.respond_to?(:close) - @log = nil + @log.close end - protected - def auto_flush - flush if buffer.size >= @auto_flushing - end - - def buffer - @buffer[Thread.current] - end - - def clear_buffer - @buffer.delete(Thread.current) - end - - # Find buffers created by threads that are no longer alive and flush them to the log - # in order to prevent memory leaks from spawned threads. - def flush_dead_buffers #:nodoc: - @buffer.keys.reject{|thread| thread.alive?}.each do |thread| - buffer = @buffer[thread] - write_buffer(buffer) - @buffer.delete(thread) - end - end - - def write_buffer(buffer) - buffer.each do |content| - @log.write(content) - end - end + private + def open_logfile(log) + Logger.new log + end end end diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 07f5fcdeb3..9711ed6f73 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -16,6 +16,7 @@ module ActiveSupport autoload :FileStore, 'active_support/cache/file_store' autoload :MemoryStore, 'active_support/cache/memory_store' autoload :MemCacheStore, 'active_support/cache/mem_cache_store' + autoload :NullStore, 'active_support/cache/null_store' # These options mean something to all cache implementations. Individual cache # implementations may support additional options. diff --git a/activesupport/lib/active_support/cache/null_store.rb b/activesupport/lib/active_support/cache/null_store.rb new file mode 100644 index 0000000000..4427eaafcd --- /dev/null +++ b/activesupport/lib/active_support/cache/null_store.rb @@ -0,0 +1,44 @@ +module ActiveSupport + module Cache + # A cache store implementation which doesn't actually store anything. Useful in + # development and test environments where you don't want caching turned on but + # need to go through the caching interface. + # + # This cache does implement the local cache strategy, so values will actually + # be cached inside blocks that utilize this strategy. See + # ActiveSupport::Cache::Strategy::LocalCache for more details. + class NullStore < Store + def initialize(options = nil) + super(options) + extend Strategy::LocalCache + end + + def clear(options = nil) + end + + def cleanup(options = nil) + end + + def increment(name, amount = 1, options = nil) + end + + def decrement(name, amount = 1, options = nil) + end + + def delete_matched(matcher, options = nil) + end + + protected + def read_entry(key, options) # :nodoc: + end + + def write_entry(key, entry, options) # :nodoc: + true + end + + def delete_entry(key, options) # :nodoc: + false + end + end + end +end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index ea37355fc1..11069301f1 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -81,6 +81,14 @@ module ActiveSupport send("_run_#{kind}_callbacks", *args, &block) end + private + + # A hook invoked everytime a before callback is halted. + # This can be overriden in AS::Callback implementors in order + # to provide better debugging/logging. + def halted_callback_hook(filter) + end + class Callback #:nodoc:# @@_callback_sequence = 0 @@ -173,12 +181,14 @@ module ActiveSupport # end <<-RUBY_EVAL if !halted && #{@compiled_options} - # This double assignment is to prevent warnings in 1.9.3. I would - # remove the `result` variable, but apparently some other - # generated code is depending on this variable being set sometimes - # and sometimes not. + # This double assignment is to prevent warnings in 1.9.3 as + # the `result` variable is not always used except if the + # terminator code refers to it. result = result = #{@filter} halted = (#{chain.config[:terminator]}) + if halted + halted_callback_hook(#{@raw_filter.inspect.inspect}) + end end RUBY_EVAL when :around diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 9343bb7106..d9856f2e84 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -63,6 +63,13 @@ module Enumerable end end + # Plucks the value of the passed method for each element and returns the result as an array. Example: + # + # people.pluck(:name) # => [ "David Heinemeier Hansson", "Jamie Heinemeier Hansson" ] + def pluck(method) + collect { |element| element.send(method) } + end + # Iterates over a collection, passing the current element *and* the # +memo+ to the block. Handy for building up hashes or # reducing collections down to one object. Examples: diff --git a/activesupport/lib/active_support/core_ext/module/synchronization.rb b/activesupport/lib/active_support/core_ext/module/synchronization.rb index ed16c2f71b..061621c0ef 100644 --- a/activesupport/lib/active_support/core_ext/module/synchronization.rb +++ b/activesupport/lib/active_support/core_ext/module/synchronization.rb @@ -1,6 +1,7 @@ require 'thread' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/array/extract_options' +require 'active_support/core_ext/module/deprecation' class Module # Synchronize access around a method, delegating synchronization to a @@ -40,4 +41,5 @@ class Module alias_method_chain method, :synchronization end end + deprecate :synchronize end diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index aae1cfccf2..400db2ce39 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -44,7 +44,7 @@ class String end end - def is_utf8? #:nodoc + def is_utf8? case encoding when Encoding::UTF_8 valid_encoding? 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 5d7f74bb65..c6d861d124 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -6,21 +6,33 @@ class ERB HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"' } JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' } - # A utility method for escaping HTML tag characters. - # This method is also aliased as <tt>h</tt>. - # - # In your ERB templates, use this method to escape any unsafe content. For example: - # <%=h @person.name %> - # - # ==== Example: - # puts html_escape("is a > 0 & a < 10?") - # # => is a > 0 & a < 10? - def html_escape(s) - s = s.to_s - if s.html_safe? - s - else - s.gsub(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<").html_safe + # Detect whether 1.9 can transcode with XML escaping. + if '"><&""' == ('><&"'.encode('utf-8', :xml => :attr) rescue false) + # A utility method for escaping HTML tag characters. + # This method is also aliased as <tt>h</tt>. + # + # In your ERB templates, use this method to escape any unsafe content. For example: + # <%=h @person.name %> + # + # ==== Example: + # puts html_escape("is a > 0 & a < 10?") + # # => is a > 0 & a < 10? + def html_escape(s) + s = s.to_s + if s.html_safe? + s + else + s.encode(s.encoding, :xml => :attr)[1...-1].html_safe + end + end + else + def html_escape(s) #:nodoc: + s = s.to_s + if s.html_safe? + s + else + s.gsub(/[&"><]/n) { |special| HTML_ESCAPE[special] }.html_safe + end end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 1372e71a61..43dd22654a 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -71,14 +71,24 @@ module ActiveSupport #:nodoc: # # This is handled by walking back up the watch stack and adding the constants # found by child.rb to the list of original constants in parent.rb - class WatchStack < Hash + class WatchStack + include Enumerable + # @watching is a stack of lists of constants being watched. For instance, # if parent.rb is autoloaded, the stack will look like [[Object]]. If parent.rb # then requires namespace/child.rb, the stack will look like [[Object], [Namespace]]. def initialize @watching = [] - super { |h,k| h[k] = [] } + @stack = Hash.new { |h,k| h[k] = [] } + end + + def each(&block) + @stack.each(&block) + end + + def watching? + !@watching.empty? end # return a list of new constants found since the last call to watch_namespaces @@ -89,7 +99,7 @@ module ActiveSupport #:nodoc: @watching.last.each do |namespace| # Retrieve the constants that were present under the namespace when watch_namespaces # was originally called - original_constants = self[namespace].last + original_constants = @stack[namespace].last mod = Inflector.constantize(namespace) if Dependencies.qualified_const_defined?(namespace) next unless mod.is_a?(Module) @@ -102,7 +112,7 @@ module ActiveSupport #:nodoc: # element of self[Object] will be an Array of the constants that were present # before parent.rb was required. The second element will be an Array of the # constants that were present before child.rb was required. - self[namespace].each do |namespace_constants| + @stack[namespace].each do |namespace_constants| namespace_constants.concat(new_constants) end @@ -126,13 +136,14 @@ module ActiveSupport #:nodoc: Inflector.constantize(module_name).local_constant_names : [] watching << module_name - self[module_name] << original_constants + @stack[module_name] << original_constants end @watching << watching end + private def pop_modules(modules) - modules.each { |mod| self[mod].pop } + modules.each { |mod| @stack[mod].pop } end end @@ -219,8 +230,8 @@ module ActiveSupport #:nodoc: end def load_dependency(file) - if Dependencies.load? - Dependencies.new_constants_in(Object) { yield }.presence + if Dependencies.load? && ActiveSupport::Dependencies.constant_watch_stack.watching? + Dependencies.new_constants_in(Object) { yield } else yield end @@ -229,13 +240,13 @@ module ActiveSupport #:nodoc: raise end - def load(file, *) + def load(file, wrap = false) result = false load_dependency(file) { result = super } result end - def require(file, *) + def require(file) result = false load_dependency(file) { result = super } result diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index f76ddff038..a4ad2da137 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -1,8 +1,28 @@ +require "active_support/core_ext/array/wrap" +require "active_support/core_ext/array/extract_options" + module ActiveSupport - # This class is responsible to track files and invoke the given block - # whenever one of these files are changed. For example, this class - # is used by Rails to reload the I18n framework whenever they are - # changed upon a new request. + # \FileUpdateChecker specifies the API used by Rails to watch files + # and control reloading. The API depends on four methods: + # + # * +initialize+ which expects two parameters and one block as + # described below; + # + # * +updated?+ which returns a boolean if there were updates in + # the filesystem or not; + # + # * +execute+ which executes the given block on initialization + # and updates the counter to the latest timestamp; + # + # * +execute_if_updated+ which just executes the block if it was updated; + # + # After initialization, a call to +execute_if_updated+ must execute + # the block only if there was really a change in the filesystem. + # + # == Examples + # + # This class is used by Rails to reload the I18n framework whenever + # they are changed upon a new request. # # i18n_reloader = ActiveSupport::FileUpdateChecker.new(paths) do # I18n.reload! @@ -13,24 +33,89 @@ module ActiveSupport # end # class FileUpdateChecker - attr_reader :paths, :last_update_at - - def initialize(paths, calculate=false, &block) - @paths = paths + # It accepts two parameters on initialization. The first is an array + # of files and the second is an optional hash of directories. The hash must + # have directories as keys and the value is an array of extensions to be + # watched under that directory. + # + # This method must also receive a block that will be called once a path changes. + # + # == Implementation details + # + # This particular implementation checks for added and updated files, + # but not removed files. Directories lookup are compiled to a glob for + # performance. Therefore, while someone can add new files to the +files+ + # array after initialization (and parts of Rails do depend on this feature), + # adding new directories after initialization is not allowed. + # + # Notice that other objects that implements FileUpdateChecker API may + # not even allow new files to be added after initialization. If this + # is the case, we recommend freezing the +files+ after initialization to + # avoid changes that won't make effect. + def initialize(files, dirs={}, &block) + @files = files + @glob = compile_glob(dirs) @block = block - @last_update_at = calculate ? updated_at : nil + @updated_at = nil + @last_update_at = updated_at + end + + # Check if any of the entries were updated. If so, the updated_at + # value is cached until the block is executed via +execute+ or +execute_if_updated+ + def updated? + current_updated_at = updated_at + if @last_update_at < current_updated_at + @updated_at = updated_at + true + else + false + end end - def updated_at - paths.map { |path| File.mtime(path) }.max + # Executes the given block and updates the counter to latest timestamp. + def execute + @last_update_at = updated_at + @block.call + ensure + @updated_at = nil end + # Execute the block given if updated. def execute_if_updated - current_update_at = self.updated_at - if @last_update_at != current_update_at - @last_update_at = current_update_at - @block.call + if updated? + execute + true + else + false + end + end + + private + + def updated_at #:nodoc: + @updated_at || begin + all = [] + all.concat @files.select { |f| File.exists?(f) } + all.concat Dir[@glob] if @glob + all.map { |path| File.mtime(path) }.max || Time.at(0) end end + + def compile_glob(hash) #:nodoc: + hash.freeze # Freeze so changes aren't accidently pushed + return if hash.empty? + + globs = [] + hash.each do |key, value| + globs << "#{key}/**/*#{compile_ext(value)}" + end + "{#{globs.join(",")}}" + end + + def compile_ext(array) #:nodoc: + array = Array.wrap(array) + return if array.empty? + ".{#{array.join(",")}}" + end end end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 636f019cd5..674e4acfd6 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -16,6 +16,10 @@ module ActiveSupport dup end + def nested_under_indifferent_access + self + end + def initialize(constructor = {}) if constructor.is_a?(Hash) super() diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 4c59fe9ac9..bbeb8d82c6 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -10,14 +10,19 @@ module I18n config.i18n.fallbacks = ActiveSupport::OrderedOptions.new def self.reloader - @reloader ||= ActiveSupport::FileUpdateChecker.new([]){ I18n.reload! } + @reloader ||= ActiveSupport::FileUpdateChecker.new(reloader_paths){ I18n.reload! } + end + + def self.reloader_paths + @reloader_paths ||= [] end # Add <tt>I18n::Railtie.reloader</tt> to ActionDispatch callbacks. Since, at this # point, no path was added to the reloader, I18n.reload! is not triggered # on to_prepare callbacks. This will only happen on the config.after_initialize # callback below. - initializer "i18n.callbacks" do + initializer "i18n.callbacks" do |app| + app.reloaders << I18n::Railtie.reloader ActionDispatch::Reloader.to_prepare do I18n::Railtie.reloader.execute_if_updated end @@ -58,8 +63,8 @@ module I18n init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks) - reloader.paths.concat I18n.load_path - reloader.execute_if_updated + reloader_paths.concat I18n.load_path + reloader.execute @i18n_inited = true end diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb index daf2a1e1d9..527cce2594 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -16,8 +16,8 @@ module ActiveSupport inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies') inflect.plural(/(x|ch|ss|sh)$/i, '\1es') inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices') - inflect.plural(/([m|l])ouse$/i, '\1ice') - inflect.plural(/([m|l])ice$/i, '\1ice') + inflect.plural(/(m|l)ouse$/i, '\1ice') + inflect.plural(/(m|l)ice$/i, '\1ice') inflect.plural(/^(ox)$/i, '\1en') inflect.plural(/^(oxen)$/i, '\1') inflect.plural(/(quiz)$/i, '\1zes') @@ -35,7 +35,7 @@ module ActiveSupport inflect.singular(/(s)eries$/i, '\1eries') inflect.singular(/(m)ovies$/i, '\1ovie') inflect.singular(/(x|ch|ss|sh)es$/i, '\1') - inflect.singular(/([m|l])ice$/i, '\1ouse') + inflect.singular(/(m|l)ice$/i, '\1ouse') inflect.singular(/(bus)es$/i, '\1') inflect.singular(/(o)es$/i, '\1') inflect.singular(/(shoe)s$/i, '\1') diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index b0fd3a3c0e..925fa2a2c7 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -281,4 +281,4 @@ class DateTime strftime('%Y/%m/%d %H:%M:%S %z') end end -end +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb index a59fc26d5d..0f3ac0c132 100644 --- a/activesupport/lib/active_support/tagged_logging.rb +++ b/activesupport/lib/active_support/tagged_logging.rb @@ -30,7 +30,7 @@ module ActiveSupport @logger.add(severity, "#{tags_text}#{message}", progname, &block) end - %w( fatal error warn info debug unkown ).each do |severity| + %w( fatal error warn info debug unknown ).each do |severity| eval <<-EOM, nil, __FILE__, __LINE__ + 1 def #{severity}(progname = nil, &block) add(Logger::#{severity.upcase}, progname, &block) @@ -38,9 +38,9 @@ module ActiveSupport EOM end - def flush(*args) + def flush @tags.delete(Thread.current) - @logger.flush(*args) if @logger.respond_to?(:flush) + @logger.flush if @logger.respond_to?(:flush) end def method_missing(method, *args) diff --git a/activesupport/lib/active_support/whiny_nil.rb b/activesupport/lib/active_support/whiny_nil.rb index 577db5018e..a065233679 100644 --- a/activesupport/lib/active_support/whiny_nil.rb +++ b/activesupport/lib/active_support/whiny_nil.rb @@ -1,21 +1,6 @@ # Extensions to +nil+ which allow for more helpful error messages for people who # are new to Rails. # -# Ruby raises NoMethodError if you invoke a method on an object that does not -# respond to it: -# -# $ ruby -e nil.destroy -# -e:1: undefined method `destroy' for nil:NilClass (NoMethodError) -# -# With these extensions, if the method belongs to the public interface of the -# classes in NilClass::WHINERS the error message suggests which could be the -# actual intended class: -# -# $ rails runner nil.destroy -# ... -# You might have expected an instance of ActiveRecord::Base. -# ... -# # NilClass#id exists in Ruby 1.8 (though it is deprecated). Since +id+ is a fundamental # method of Active Record models NilClass#id is redefined as well to raise a RuntimeError # and warn the user. She probably wanted a model database identifier and the 4 @@ -25,36 +10,13 @@ # By default it is on in development and test modes, and it is off in production # mode. class NilClass - METHOD_CLASS_MAP = Hash.new - def self.add_whiner(klass) - methods = klass.public_instance_methods - public_instance_methods - class_name = klass.name - methods.each { |method| METHOD_CLASS_MAP[method.to_sym] = class_name } + ActiveSupport::Deprecation.warn "NilClass.add_whiner is deprecated and this functionality is " \ + "removed from Rails versions as it affects Ruby 1.9 performance.", caller end - add_whiner ::Array - # Raises a RuntimeError when you attempt to call +id+ on +nil+. def id raise RuntimeError, "Called id for nil, which would mistakenly be #{object_id} -- if you really wanted the id of nil, use object_id", caller end - - private - def method_missing(method, *args) - if klass = METHOD_CLASS_MAP[method] - raise_nil_warning_for klass, method, caller - else - super - end - end - - # Raises a NoMethodError when you attempt to call a method on +nil+. - def raise_nil_warning_for(class_name = nil, selector = nil, with_caller = nil) - message = "You have a nil object when you didn't expect it!" - message << "\nYou might have expected an instance of #{class_name}." if class_name - message << "\nThe error occurred while evaluating nil.#{selector}" if selector - - raise NoMethodError, message, with_caller || caller - end end diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index 93a8474c05..6c222b83ba 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -70,7 +70,7 @@ module ActiveSupport hash = get_attributes(element) child_nodes = element.child_nodes - if child_nodes.any? + if child_nodes.length > 0 for i in 0...child_nodes.length child = child_nodes.item(i) merge_element!(hash, child) unless child.node_type == Node.TEXT_NODE diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb index fb539e9c88..04ec9e8ab8 100644 --- a/activesupport/lib/active_support/xml_mini/nokogiri.rb +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -26,7 +26,7 @@ module ActiveSupport else data.ungetc(char) doc = Nokogiri::XML(data) - raise doc.errors.first if doc.errors.any? + raise doc.errors.first if doc.errors.length > 0 doc.to_hash end end diff --git a/activesupport/test/benchmarkable_test.rb b/activesupport/test/benchmarkable_test.rb index 06f5172e1f..24b5b5bee1 100644 --- a/activesupport/test/benchmarkable_test.rb +++ b/activesupport/test/benchmarkable_test.rb @@ -3,8 +3,23 @@ require 'abstract_unit' class BenchmarkableTest < ActiveSupport::TestCase include ActiveSupport::Benchmarkable - def teardown - logger.send(:clear_buffer) + attr_reader :buffer, :logger + + class Buffer + include Enumerable + + def initialize; @lines = []; end + def each(&block); @lines.each(&block); end + def write(x); @lines << x; end + def close; end + def last; @lines.last; end + def size; @lines.size; end + def empty?; @lines.empty?; end + end + + def setup + @buffer = Buffer.new + @logger = ActiveSupport::BufferedLogger.new(@buffer) end def test_without_block @@ -40,35 +55,7 @@ class BenchmarkableTest < ActiveSupport::TestCase logger.level = ActiveSupport::BufferedLogger::DEBUG end - def test_without_silencing - benchmark('debug_run', :silence => false) do - logger.info "not silenced!" - end - - assert_equal 2, buffer.size - end - - def test_with_silencing - benchmark('debug_run', :silence => true) do - logger.info "silenced!" - end - - assert_equal 1, buffer.size - end - private - def logger - @logger ||= begin - logger = ActiveSupport::BufferedLogger.new(StringIO.new) - logger.auto_flushing = false - logger - end - end - - def buffer - logger.send(:buffer) - end - def assert_last_logged(message = 'Benchmarking') assert_match(/^#{message} \(.*\)$/, buffer.last) end diff --git a/activesupport/test/buffered_logger_test.rb b/activesupport/test/buffered_logger_test.rb index 386006677b..f975685ca5 100644 --- a/activesupport/test/buffered_logger_test.rb +++ b/activesupport/test/buffered_logger_test.rb @@ -4,9 +4,11 @@ require 'stringio' require 'fileutils' require 'tempfile' require 'active_support/buffered_logger' +require 'active_support/testing/deprecation' class BufferedLoggerTest < Test::Unit::TestCase include MultibyteTestHelpers + include ActiveSupport::Testing::Deprecation Logger = ActiveSupport::BufferedLogger @@ -23,7 +25,10 @@ class BufferedLoggerTest < Test::Unit::TestCase t.write 'hi mom!' t.close - logger = Logger.new t.path + f = File.open(t.path, 'w') + f.binmode + + logger = Logger.new f logger.level = Logger::DEBUG str = "\x80" @@ -32,7 +37,6 @@ class BufferedLoggerTest < Test::Unit::TestCase end logger.add Logger::DEBUG, str - logger.flush ensure logger.close t.close true @@ -40,7 +44,11 @@ class BufferedLoggerTest < Test::Unit::TestCase def test_write_binary_data_create_file fname = File.join Dir.tmpdir, 'lol', 'rofl.log' - logger = Logger.new fname + FileUtils.mkdir_p File.dirname(fname) + f = File.open(fname, 'w') + f.binmode + + logger = Logger.new f logger.level = Logger::DEBUG str = "\x80" @@ -49,7 +57,6 @@ class BufferedLoggerTest < Test::Unit::TestCase end logger.add Logger::DEBUG, str - logger.flush ensure logger.close File.unlink fname @@ -104,34 +111,6 @@ class BufferedLoggerTest < Test::Unit::TestCase assert_equal message_copy, @message end - - [false, nil, 0].each do |disable| - define_method "test_disabling_auto_flush_with_#{disable.inspect}_should_buffer_until_explicit_flush" do - @logger.auto_flushing = disable - - 4.times do - @logger.info 'wait for it..' - assert @output.string.empty?, "@output.string should be empty but it is #{@output.string}" - end - - @logger.flush - assert !@output.string.empty?, "@logger.send(:buffer).size.to_s should not be empty but it is empty" - end - - define_method "test_disabling_auto_flush_with_#{disable.inspect}_should_flush_at_max_buffer_size_as_failsafe" do - @logger.auto_flushing = disable - assert_equal Logger::MAX_BUFFER_SIZE, @logger.auto_flushing - - (Logger::MAX_BUFFER_SIZE - 1).times do - @logger.info 'wait for it..' - assert @output.string.empty?, "@output.string should be empty but is #{@output.string}" - end - - @logger.info 'there it is.' - assert !@output.string.empty?, "@logger.send(:buffer).size.to_s should not be empty but it is empty" - end - end - def test_should_know_if_its_loglevel_is_below_a_given_level Logger::Severity.constants.each do |level| @logger.level = Logger::Severity.const_get(level) - 1 @@ -139,56 +118,17 @@ class BufferedLoggerTest < Test::Unit::TestCase end end - def test_should_auto_flush_every_n_messages - @logger.auto_flushing = 5 - - 4.times do - @logger.info 'wait for it..' - assert @output.string.empty?, "@output.string should be empty but it is #{@output.string}" - end - - @logger.info 'there it is.' - assert !@output.string.empty?, "@output.string should not be empty but it is empty" - end - def test_should_create_the_log_directory_if_it_doesnt_exist tmp_directory = File.join(File.dirname(__FILE__), "tmp") log_file = File.join(tmp_directory, "development.log") FileUtils.rm_rf(tmp_directory) - @logger = Logger.new(log_file) - assert File.exist?(tmp_directory) - end - - def test_logger_should_maintain_separate_buffers_for_each_thread - @logger.auto_flushing = false - - a = Thread.new do - @logger.info("a"); Thread.pass; - @logger.info("b"); Thread.pass; - @logger.info("c"); @logger.flush - end - - b = Thread.new do - @logger.info("x"); Thread.pass; - @logger.info("y"); Thread.pass; - @logger.info("z"); @logger.flush + assert_deprecated do + @logger = Logger.new(log_file) end - - a.join - b.join - - assert @output.string.include?("a\nb\nc\n") - assert @output.string.include?("x\ny\nz\n") - end - - def test_flush_should_remove_empty_buffers - @logger.send :buffer - @logger.expects :clear_buffer - @logger.flush + assert File.exist?(tmp_directory) end def test_buffer_multibyte - @logger.auto_flushing = 2 @logger.info(UNICODE_STRING) @logger.info(BYTE_STRING) assert @output.string.include?(UNICODE_STRING) @@ -198,57 +138,4 @@ class BufferedLoggerTest < Test::Unit::TestCase end assert byte_string.include?(BYTE_STRING) end - - def test_silence_only_current_thread - @logger.auto_flushing = true - run_thread_a = false - - a = Thread.new do - while !run_thread_a do - sleep(0.001) - end - @logger.info("x") - run_thread_a = false - end - - @logger.silence do - run_thread_a = true - @logger.info("a") - while run_thread_a do - sleep(0.001) - end - end - - a.join - - assert @output.string.include?("x") - assert !@output.string.include?("a") - end - - def test_flush_dead_buffers - @logger.auto_flushing = false - - a = Thread.new do - @logger.info("a") - end - - keep_running = true - Thread.new do - @logger.info("b") - while keep_running - sleep(0.001) - end - end - - @logger.info("x") - a.join - @logger.flush - - - assert @output.string.include?("x") - assert @output.string.include?("a") - assert !@output.string.include?("b") - - keep_running = false - end end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 5d7464c623..ab29ddecc5 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -218,7 +218,7 @@ module CacheStoreBehavior @cache.write('fud', 'biz') assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu')) end - + def test_read_multi_with_expires @cache.write('foo', 'bar', :expires_in => 0.001) @cache.write('fu', 'baz') @@ -576,12 +576,12 @@ class FileStoreTest < ActiveSupport::TestCase key = @cache_with_pathname.send(:key_file_path, "views/index?id=1") assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key) end - + # Because file systems have a maximum filename size, filenames > max size should be split in to directories # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B def test_key_transformation_max_filename_size key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B" - path = @cache.send(:key_file_path, key) + path = @cache.send(:key_file_path, key) assert path.split('/').all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE} assert_equal 'B', File.basename(path) end @@ -689,14 +689,14 @@ uses_memcached 'memcached backed store' do cache.write("foo", 2) assert_equal "2", cache.read("foo") end - + def test_raw_values_with_marshal cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true) cache.clear cache.write("foo", Marshal.dump([])) - assert_equal [], cache.read("foo") + assert_equal [], cache.read("foo") end - + def test_local_cache_raw_values cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true) cache.clear @@ -717,6 +717,64 @@ uses_memcached 'memcached backed store' do end end +class NullStoreTest < ActiveSupport::TestCase + def setup + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + def test_clear + @cache.clear + end + + def test_cleanup + @cache.cleanup + end + + def test_write + assert_equal true, @cache.write("name", "value") + end + + def test_read + @cache.write("name", "value") + assert_nil @cache.read("name") + end + + def test_delete + @cache.write("name", "value") + assert_equal false, @cache.delete("name") + end + + def test_increment + @cache.write("name", 1, :raw => true) + assert_nil @cache.increment("name") + end + + def test_decrement + @cache.write("name", 1, :raw => true) + assert_nil @cache.increment("name") + end + + def test_delete_matched + @cache.write("name", "value") + @cache.delete_matched(/name/) + end + + def test_local_store_strategy + @cache.with_local_cache do + @cache.write("name", "value") + assert_equal "value", @cache.read("name") + @cache.delete("name") + assert_nil @cache.read("name") + @cache.write("name", "value") + end + assert_nil @cache.read("name") + end + + def test_setting_nil_cache_store + assert ActiveSupport::Cache.lookup_store.class.name, ActiveSupport::Cache::NullStore.name + end +end + class CacheStoreLoggerTest < ActiveSupport::TestCase def setup @cache = ActiveSupport::Cache.lookup_store(:memory_store) diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 2b4adda4d1..e723121bb4 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -461,7 +461,7 @@ module CallbacksTest set_callback :save, :after, :third - attr_reader :history, :saved + attr_reader :history, :saved, :halted def initialize @history = [] end @@ -490,6 +490,10 @@ module CallbacksTest @saved = true end end + + def halted_callback_hook(filter) + @halted = filter + end end class CallbackObject @@ -595,6 +599,12 @@ module CallbacksTest assert_equal ["first", "second", "third", "second", "first"], terminator.history end + def test_termination_invokes_hook + terminator = CallbackTerminator.new + terminator.save + assert_equal ":second", terminator.halted + end + def test_block_never_called_if_terminated obj = CallbackTerminator.new obj.save diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index cdfa991a34..7ce7c4d52d 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -126,4 +126,11 @@ class EnumerableTests < Test::Unit::TestCase assert_equal true, GenericEnumerable.new([ 1 ]).exclude?(2) assert_equal false, GenericEnumerable.new([ 1 ]).exclude?(1) end -end + + def test_pluck_single_method + person = Struct.new(:name) + people = [ person.new("David"), person.new("Jamie") ] + + assert_equal [ "David", "Jamie" ], people.pluck(:name) + end +end
\ No newline at end of file diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index fa800eada2..eb8ed761da 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -121,6 +121,9 @@ class HashExtTest < Test::Unit::TestCase foo = { "foo" => NonIndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access assert_kind_of NonIndifferentHash, foo["foo"] + + foo = { "foo" => IndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access + assert_kind_of IndifferentHash, foo["foo"] end def test_indifferent_assorted diff --git a/activesupport/test/core_ext/module/synchronization_test.rb b/activesupport/test/core_ext/module/synchronization_test.rb deleted file mode 100644 index 6c407e2260..0000000000 --- a/activesupport/test/core_ext/module/synchronization_test.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'thread' -require 'abstract_unit' - -require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/core_ext/module/synchronization' - -class SynchronizationTest < Test::Unit::TestCase - def setup - @target = Class.new - @target.cattr_accessor :mutex, :instance_writer => false - @target.mutex = Mutex.new - @instance = @target.new - end - - def test_synchronize_aliases_method_chain_with_synchronize - @target.module_eval do - attr_accessor :value - synchronize :value, :with => :mutex - end - assert_respond_to @instance, :value_with_synchronization - assert_respond_to @instance, :value_without_synchronization - end - - def test_synchronize_does_not_change_behavior - @target.module_eval do - attr_accessor :value - synchronize :value, :with => :mutex - end - expected = "some state" - @instance.value = expected - assert_equal expected, @instance.value - end - - def test_synchronize_with_no_mutex_raises_an_argument_error - assert_raise(ArgumentError) do - @target.synchronize :to_s - end - end - - def test_double_synchronize_raises_an_argument_error - @target.synchronize :to_s, :with => :mutex - assert_raise(ArgumentError) do - @target.synchronize :to_s, :with => :mutex - end - end - - def dummy_sync - dummy = Object.new - def dummy.synchronize - @sync_count ||= 0 - @sync_count += 1 - yield - end - def dummy.sync_count; @sync_count; end - dummy - end - - def test_mutex_is_entered_during_method_call - @target.mutex = dummy_sync - @target.synchronize :to_s, :with => :mutex - @instance.to_s - @instance.to_s - assert_equal 2, @target.mutex.sync_count - end - - def test_can_synchronize_method_with_punctuation - @target.module_eval do - def dangerous? - @dangerous - end - def dangerous! - @dangerous = true - end - end - @target.synchronize :dangerous?, :dangerous!, :with => :mutex - @instance.dangerous! - assert @instance.dangerous? - end - - def test_can_synchronize_singleton_methods - @target.mutex = dummy_sync - class << @target - synchronize :to_s, :with => :mutex - end - assert_respond_to @target, :to_s_without_synchronization - assert_nothing_raised { @target.to_s; @target.to_s } - assert_equal 2, @target.mutex.sync_count - end -end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index ade09efc56..47b9f68ed0 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -21,12 +21,6 @@ class StringInflectionsTest < Test::Unit::TestCase include InflectorTestCases include ConstantizeTestCases - def test_erb_escape - string = [192, 60].pack('CC') - expected = 192.chr + "<" - assert_equal expected, ERB::Util.html_escape(string) - end - def test_strip_heredoc_on_an_empty_string assert_equal '', ''.strip_heredoc end @@ -497,6 +491,23 @@ class OutputSafetyTest < ActiveSupport::TestCase assert string.html_safe? assert !string.to_param.html_safe? end + + test "ERB::Util.html_escape should escape unsafe characters" do + string = '<>&"' + expected = '<>&"' + assert_equal expected, ERB::Util.html_escape(string) + end + + test "ERB::Util.html_escape should correctly handle invalid UTF-8 strings" do + string = [192, 60].pack('CC') + expected = 192.chr + "<" + assert_equal expected, ERB::Util.html_escape(string) + end + + test "ERB::Util.html_escape should not escape safe strings" do + string = "<b>hello</b>".html_safe + assert_equal string, ERB::Util.html_escape(string) + end end class StringExcludeTest < ActiveSupport::TestCase diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index fe8f51e11a..7039f90499 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -258,6 +258,85 @@ class DependenciesTest < Test::Unit::TestCase $:.replace(original_path) end + def test_require_returns_true_when_file_not_yet_required + path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + original_path = $:.dup + original_features = $".dup + $:.push(path) + + with_loading do + assert_equal true, require('loaded_constant') + end + ensure + remove_constants(:LoadedConstant) + $".replace(original_features) + $:.replace(original_path) + end + + def test_require_returns_true_when_file_not_yet_required_even_when_no_new_constants_added + path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + original_path = $:.dup + original_features = $".dup + $:.push(path) + + with_loading do + Object.module_eval "module LoadedConstant; end" + assert_equal true, require('loaded_constant') + end + ensure + remove_constants(:LoadedConstant) + $".replace(original_features) + $:.replace(original_path) + end + + def test_require_returns_false_when_file_already_required + path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + original_path = $:.dup + original_features = $".dup + $:.push(path) + + with_loading do + require 'loaded_constant' + assert_equal false, require('loaded_constant') + end + ensure + remove_constants(:LoadedConstant) + $".replace(original_features) + $:.replace(original_path) + end + + def test_require_raises_load_error_when_file_not_found + with_loading do + assert_raise(LoadError) { require 'this_file_dont_exist_dude' } + end + ensure + remove_constants(:LoadedConstant) + end + + def test_load_returns_true_when_file_found + path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + original_path = $:.dup + original_features = $".dup + $:.push(path) + + with_loading do + assert_equal true, load('loaded_constant.rb') + assert_equal true, load('loaded_constant.rb') + end + ensure + remove_constants(:LoadedConstant) + $".replace(original_features) + $:.replace(original_path) + end + + def test_load_raises_load_error_when_file_not_found + with_loading do + assert_raise(LoadError) { load 'this_file_dont_exist_dude.rb' } + end + ensure + remove_constants(:LoadedConstant) + end + def failing_test_access_thru_and_upwards_fails with_autoloading_fixtures do assert ! defined?(ModuleFolder) diff --git a/activesupport/test/file_update_checker_test.rb b/activesupport/test/file_update_checker_test.rb index b65bb1d024..dd19b58aa2 100644 --- a/activesupport/test/file_update_checker_test.rb +++ b/activesupport/test/file_update_checker_test.rb @@ -4,15 +4,17 @@ require 'fileutils' MTIME_FIXTURES_PATH = File.expand_path("../fixtures", __FILE__) -class FileUpdateCheckerTest < Test::Unit::TestCase +class FileUpdateCheckerWithEnumerableTest < Test::Unit::TestCase FILES = %w(1.txt 2.txt 3.txt) def setup + FileUtils.mkdir_p("tmp_watcher") FileUtils.touch(FILES) end def teardown - FileUtils.rm(FILES) + FileUtils.rm_rf("tmp_watcher") + FileUtils.rm_rf(FILES) end def test_should_not_execute_the_block_if_no_paths_are_given @@ -22,34 +24,60 @@ class FileUpdateCheckerTest < Test::Unit::TestCase assert_equal 0, i end - def test_should_invoke_the_block_on_first_call_if_it_does_not_calculate_last_updated_at_on_load + def test_should_not_invoke_the_block_if_no_file_has_changed i = 0 checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - checker.execute_if_updated - assert_equal 1, i + 5.times { assert !checker.execute_if_updated } + assert_equal 0, i end - def test_should_not_invoke_the_block_on_first_call_if_it_calculates_last_updated_at_on_load + def test_should_invoke_the_block_if_a_file_has_changed i = 0 - checker = ActiveSupport::FileUpdateChecker.new(FILES, true){ i += 1 } - checker.execute_if_updated - assert_equal 0, i + checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } + sleep(1) + FileUtils.touch(FILES) + assert checker.execute_if_updated + assert_equal 1, i end - def test_should_not_invoke_the_block_if_no_file_has_changed + def test_should_be_robust_enough_to_handle_deleted_files i = 0 checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - 5.times { checker.execute_if_updated } - assert_equal 1, i + FileUtils.rm(FILES) + assert !checker.execute_if_updated + assert_equal 0, i end - def test_should_invoke_the_block_if_a_file_has_changed + def test_should_cache_updated_result_until_execute i = 0 checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - checker.execute_if_updated + assert !checker.updated? + sleep(1) FileUtils.touch(FILES) - checker.execute_if_updated - assert_equal 2, i + + assert checker.updated? + checker.execute + assert !checker.updated? + end + + def test_should_invoke_the_block_if_a_watched_dir_changed_its_glob + i = 0 + checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => [:txt]){ i += 1 } + FileUtils.cd "tmp_watcher" do + FileUtils.touch(FILES) + end + assert checker.execute_if_updated + assert_equal 1, i + end + + def test_should_not_invoke_the_block_if_a_watched_dir_changed_its_glob + i = 0 + checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => :rb){ i += 1 } + FileUtils.cd "tmp_watcher" do + FileUtils.touch(FILES) + end + assert !checker.execute_if_updated + assert_equal 0, i end -end +end
\ No newline at end of file diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index e3a343af52..eb2915c286 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -104,7 +104,11 @@ module InflectorTestCases "edge" => "edges", "cow" => "kine", - "database" => "databases" + "database" => "databases", + + # regression tests against improper inflection regexes + "|ice" => "|ices", + "|ouse" => "|ouses" } CamelToUnderscore = { diff --git a/activesupport/test/whiny_nil_test.rb b/activesupport/test/whiny_nil_test.rb index 1acaf7228f..8c1f1b2677 100644 --- a/activesupport/test/whiny_nil_test.rb +++ b/activesupport/test/whiny_nil_test.rb @@ -1,54 +1,11 @@ -# Stub to enable testing without Active Record -module ActiveRecord - class Base - def save! - end - end -end - require 'abstract_unit' require 'active_support/whiny_nil' -NilClass.add_whiner ::ActiveRecord::Base - class WhinyNilTest < Test::Unit::TestCase - def test_unchanged - nil.method_thats_not_in_whiners - rescue NoMethodError => nme - assert_match(/nil:NilClass/, nme.message) - end - - def test_active_record - nil.save! - rescue NoMethodError => nme - assert_no_match(/nil:NilClass/, nme.message) - assert_match(/nil\.save!/, nme.message) - end - - def test_array - nil.each - rescue NoMethodError => nme - assert_no_match(/nil:NilClass/, nme.message) - assert_match(/nil\.each/, nme.message) - end - def test_id nil.id rescue RuntimeError => nme assert_no_match(/nil:NilClass/, nme.message) assert_match(Regexp.new(nil.object_id.to_s), nme.message) end - - def test_no_to_ary_coercion - nil.to_ary - rescue NoMethodError => nme - assert_no_match(/nil:NilClass/, nme.message) - assert_match(/nil\.to_ary/, nme.message) - end - - def test_no_to_str_coercion - nil.to_str - rescue NoMethodError => nme - assert_match(/nil:NilClass/, nme.message) - end end diff --git a/ci/travis.rb b/ci/travis.rb index 8087c72f90..52e146df70 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -132,7 +132,7 @@ failures = results.select { |key, value| value == false } if failures.empty? puts - puts "Rails build finished sucessfully" + puts "Rails build finished successfully" exit(true) else puts diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 6b0be4c096..0bd76df6d7 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,13 +1,21 @@ ## Rails 3.2.0 (unreleased) ## -* Display mounted engine's routes in `rake routes`. *Piotr Sarnacki* +* Added `config.exceptions_app` to set the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to `ActionDispatch::PublicExceptions.new(Rails.public_path)`. *José Valim* + +* Speed up development by only reloading classes if dependencies files changed. This can be turned off by setting `config.reload_classes_only_on_change` to false. *José Valim* + +* New applications get a flag `config.active_record.auto_explain_threshold_in_seconds` in the environments configuration files. With a value of 0.5 in development.rb, and commented out in production.rb. No mention in test.rb. *fxn* + +* Add DebugExceptions middleware which contains features extracted from ShowExceptions middleware *José Valim* + +* Display mounted engine's routes in `rake routes` *Piotr Sarnacki* * Allow to change the loading order of railties with `config.railties_order=` *Piotr Sarnacki* Example: config.railties_order = [Blog::Engine, :main_app, :all] -* Scaffold returns 204 No Content for API requests without content. This makes scaffold work with jQuery out of the box. *José Valim* +* Scaffold returns 204 No Content for API requests without content. This makes scaffold work with jQuery out of the box *José Valim* * Update Rails::Rack::Logger middleware to apply any tags set in config.log_tags to the newly ActiveSupport::TaggedLogging Rails.logger. This makes it easy to tag log lines with debug information like subdomain and request id -- both very helpful in debugging multi-user production applications *DHH* diff --git a/railties/guides/code/getting_started/README b/railties/guides/code/getting_started/README.rdoc index 7c36f2356e..7c36f2356e 100644 --- a/railties/guides/code/getting_started/README +++ b/railties/guides/code/getting_started/README.rdoc diff --git a/railties/guides/code/getting_started/config/environments/development.rb b/railties/guides/code/getting_started/config/environments/development.rb index 89932bf19b..d60a10568b 100644 --- a/railties/guides/code/getting_started/config/environments/development.rb +++ b/railties/guides/code/getting_started/config/environments/development.rb @@ -22,6 +22,13 @@ Blog::Application.configure do # Only use best-standards-support built into browsers config.action_dispatch.best_standards_support = :builtin + # Raise exception on mass assignment protection for ActiveRecord models + config.active_record.mass_assignment_sanitizer = :strict + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + config.active_record.auto_explain_threshold_in_seconds = 0.5 + # Do not compress assets config.assets.compress = false diff --git a/railties/guides/code/getting_started/config/environments/production.rb b/railties/guides/code/getting_started/config/environments/production.rb index dee8acfdfe..c9b2f41c39 100644 --- a/railties/guides/code/getting_started/config/environments/production.rb +++ b/railties/guides/code/getting_started/config/environments/production.rb @@ -60,4 +60,8 @@ Blog::Application.configure do # Send deprecation notices to registered listeners config.active_support.deprecation = :notify + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + # config.active_record.auto_explain_threshold_in_seconds = 0.5 end diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile index b34c223b31..bc85f07ecc 100644 --- a/railties/guides/source/action_controller_overview.textile +++ b/railties/guides/source/action_controller_overview.textile @@ -16,7 +16,7 @@ h3. What Does a Controller Do? Action Controller is the C in MVC. After routing has determined which controller to use for a request, your controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. -For most conventional RESTful applications, the controller will receive the request (this is invisible to you as the developer), fetch or save data from a model and use a view to create HTML output. If your controller needs to do things a little differently, that's not a problem, this is just the most common way for a controller to work. +For most conventional "RESTful":http://en.wikipedia.org/wiki/Representational_state_transfer applications, the controller will receive the request (this is invisible to you as the developer), fetch or save data from a model and use a view to create HTML output. If your controller needs to do things a little differently, that's not a problem, this is just the most common way for a controller to work. A controller can thus be thought of as a middle man between models and views. It makes the model data available to the view so it can display that data to the user, and it saves or updates data from the user to the model. diff --git a/railties/guides/source/active_record_basics.textile b/railties/guides/source/active_record_basics.textile index 66ad7b0255..487f8b70f9 100644 --- a/railties/guides/source/active_record_basics.textile +++ b/railties/guides/source/active_record_basics.textile @@ -101,11 +101,11 @@ h3. Overriding the Naming Conventions What if you need to follow a different naming convention or need to use your Rails application with a legacy database? No problem, you can easily override the default conventions. -You can use the +ActiveRecord::Base.set_table_name+ method to specify the table name that should be used: +You can use the +ActiveRecord::Base.table_name=+ method to specify the table name that should be used: <ruby> class Product < ActiveRecord::Base - set_table_name "PRODUCT" + self.table_name = "PRODUCT" end </ruby> diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile index c4724f182e..0cbabd71a1 100644 --- a/railties/guides/source/active_record_querying.textile +++ b/railties/guides/source/active_record_querying.textile @@ -1146,6 +1146,30 @@ h3. +select_all+ Client.connection.select_all("SELECT * FROM clients WHERE id = '1'") </ruby> +h3. +pluck+ + +<tt>pluck</tt> can be used to query a single column from the underlying table of a model. It accepts a column name as argument and returns an array of values of the specified column with the corresponding data type. + +<ruby> +Client.where(:active => true).pluck(:id) +# SELECT id FROM clients WHERE active = 1 + +Client.uniq.pluck(:role) +# SELECT DISTINCT role FROM clients +</ruby> + ++pluck+ makes it possible to replace code like + +<ruby> +Client.select(:id).map { |c| c.id } +</ruby> + +with + +<ruby> +Client.pluck(:id) +</ruby> + h3. Existence of Objects If you simply want to check for the existence of the object there's a method called +exists?+. This method will query the database using the same query as +find+, but instead of returning an object or collection of objects it will return either +true+ or +false+. @@ -1345,6 +1369,42 @@ EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1) under MySQL. +h4. Automatic EXPLAIN + +Active Record is able to run EXPLAIN automatically on slow queries and log its +output. This feature is controlled by the configuration parameter + +<ruby> +config.active_record.auto_explain_threshold_in_seconds +</ruby> + +If set to a number, any query exceeding those many seconds will have its EXPLAIN +automatically triggered and logged. In the case of relations, the threshold is +compared to the total time needed to fetch records. So, a relation is seen as a +unit of work, no matter whether the implementation of eager loading involves +several queries under the hood. + +A threshold of +nil+ disables automatic EXPLAINs. + +The default threshold in development mode is 0.5 seconds, and +nil+ in test and +production modes. + +h5. Disabling Automatic EXPLAIN + +Automatic EXPLAIN can be selectively silenced with +ActiveRecord::Base.silence_auto_explain+: + +<ruby> +ActiveRecord::Base.silence_auto_explain do + # no automatic EXPLAIN is triggered here +end +</ruby> + +That may be useful for queries you know are slow but fine, like a heavyweight +report of an admin interface. + +As its name suggests, +silence_auto_explain+ only silences automatic EXPLAINs. +Explicit calls to +ActiveRecord::Relation#explain+ run. + h4. Interpreting EXPLAIN Interpretation of the output of EXPLAIN is beyond the scope of this guide. The diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index 2dee440b3b..6646e9cd05 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -440,14 +440,16 @@ NOTE: Defined in +active_support/core_ext/kernel/reporting.rb+. h4. +in?+ -The predicate +in?+ tests if an object is included in another object. An +ArgumentError+ exception will be raised if the argument passed does not respond to +include?+. +The predicate +in?+ tests if an object is included in another object or a list of objects. An +ArgumentError+ exception will be raised if a single argument is passed and it does not respond to +include?+. Examples of +in?+: <ruby> +1.in?(1,2) # => true 1.in?([1,2]) # => true "lo".in?("hello") # => true 25.in?(30..50) # => false +1.in?(1) # => ArgumentError </ruby> NOTE: Defined in +active_support/core_ext/object/inclusion.rb+. @@ -1299,7 +1301,7 @@ NOTE: Defined in +active_support/core_ext/string/output_safety.rb+. h5. Transformation -As a rule of thumb, except perhaps for concatenation as explained above, any method that may change a string gives you an unsafe string. These are +donwcase+, +gsub+, +strip+, +chomp+, +underscore+, etc. +As a rule of thumb, except perhaps for concatenation as explained above, any method that may change a string gives you an unsafe string. These are +downcase+, +gsub+, +strip+, +chomp+, +underscore+, etc. In the case of in-place transformations like +gsub!+ the receiver itself becomes unsafe. @@ -2051,6 +2053,16 @@ end NOTE: Defined in +active_support/core_ext/enumerable.rb+. +h4. +pluck+ + +The +pluck+ method collects the value of the passed method for each element and returns the result as an array. + +<ruby> +people.pluck(:name) # => [ "David Heinemeier Hansson", "Jamie Heinemeier Hansson" ] +</ruby> + +NOTE: Defined in +active_support/core_ext/enumerable.rb+. + h4. +each_with_object+ The +inject+ method offers iteration with an accumulator: diff --git a/railties/guides/source/asset_pipeline.textile b/railties/guides/source/asset_pipeline.textile index 3681501293..f48f5afd54 100644 --- a/railties/guides/source/asset_pipeline.textile +++ b/railties/guides/source/asset_pipeline.textile @@ -369,10 +369,10 @@ It is important that this folder is shared between deployments so that remotely NOTE. If you are precompiling your assets locally, you can use +bundle install --without assets+ on the server to avoid installing the assets gems (the gems in the assets group in the Gemfile). -The default matcher for compiling files includes +application.js+, +application.css+ and all files that do not end in +js+ or +css+: +The default matcher for compiling files includes +application.js+, +application.css+ and all non-JS/CSS files (ie. +.coffee+ and +.scss+ files are *not* automatically included as they compile to JS/CSS): <ruby> -[ /\w<plus>\.(?!js|css).<plus>/, /application.(css|js)$/ ] +[ Proc.new{ |path| !File.extname(path).in?(['.js', '.css']) }, /application.(css|js)$/ ] </ruby> If you have other manifests or individual stylesheets and JavaScript files to include, you can add them to the +precompile+ array: @@ -410,10 +410,6 @@ For Apache: <plain> <LocationMatch "^/assets/.*$"> - # Some browsers still send conditional-GET requests if there's a - # Last-Modified header or an ETag header even if they haven't - # reached the expiry date sent in the Expires header. - Header unset Last-Modified Header unset ETag FileETag None # RFC says only cache for 1 year @@ -429,10 +425,6 @@ location ~ ^/assets/ { expires 1y; add_header Cache-Control public; - # Some browsers still send conditional-GET requests if there's a - # Last-Modified header or an ETag header even if they haven't - # reached the expiry date sent in the Expires header. - add_header Last-Modified ""; add_header ETag ""; break; } @@ -577,7 +569,7 @@ TODO: Registering gems on "Tilt":https://github.com/rtomayko/tilt enabling Sproc h3. Upgrading from Old Versions of Rails -There are two issues when upgrading. The first is moving the files to the new locations. See the section above for guidance on the correct locations for different file types. +There are two issues when upgrading. The first is moving the files to the new locations. See "Asset Organization":#asset-organization above for guidance on the correct locations for different file types. The second is updating the various environment files with the correct default options. The following changes reflect the defaults in version 3.1.0. diff --git a/railties/guides/source/caching_with_rails.textile b/railties/guides/source/caching_with_rails.textile index 0ef6f51190..ec9bfd4d40 100644 --- a/railties/guides/source/caching_with_rails.textile +++ b/railties/guides/source/caching_with_rails.textile @@ -332,6 +332,14 @@ caches_action :index, :expires_in => 60.seconds, :unless_exist => true For more information about Ehcache, see "http://ehcache.org/":http://ehcache.org/ . For more information about Ehcache for JRuby and Rails, see "http://ehcache.org/documentation/jruby.html":http://ehcache.org/documentation/jruby.html +h4. ActiveSupport::Cache::NullStore + +This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with +Rails.cache+, but caching may interfere with being able to see the results of code changes. With this cache store, all +fetch+ and +read+ operations will result in a miss. + +<ruby> +ActionController::Base.cache_store = :null +</ruby> + h4. Custom Cache Stores You can create your own custom cache store by simply extending +ActiveSupport::Cache::Store+ and implementing the appropriate methods. In this way, you can swap in any number of caching technologies into your Rails application. diff --git a/railties/guides/source/command_line.textile b/railties/guides/source/command_line.textile index 3f8643eca3..58855bc80b 100644 --- a/railties/guides/source/command_line.textile +++ b/railties/guides/source/command_line.textile @@ -36,7 +36,7 @@ WARNING: You can install the rails gem by typing +gem install rails+, if you don <shell> $ rails new commandsapp create - create README + create README.rdoc create .gitignore create Rakefile create config.ru @@ -81,6 +81,8 @@ The server can be run on a different port using the +-p+ option. The default dev $ rails server -e production -p 4000 </shell> +The +-b+ option binds Rails to the specified ip, by default it is 0.0.0.0. You can run a server as a daemon by passing a +-d+ option. + h4. +rails generate+ The +rails generate+ command uses templates to create a whole lot of things. Running +rails generate+ by itself gives a list of available generators: @@ -389,7 +391,7 @@ Action Pack version 3.2.0.beta Active Resource version 3.2.0.beta Action Mailer version 3.2.0.beta Active Support version 3.2.0.beta -Middleware ActionDispatch::Static, Rack::Lock, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, ActionDispatch::Head, Rack::ConditionalGet, Rack::ETag, ActionDispatch::BestStandardsSupport +Middleware ActionDispatch::Static, Rack::Lock, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, ActionDispatch::Head, Rack::ConditionalGet, Rack::ETag, ActionDispatch::BestStandardsSupport Application root /home/foobar/commandsapp Environment development Database adapter sqlite3 @@ -418,7 +420,7 @@ The +doc:+ namespace has the tools to generate documentation for your app, API d h4. +notes+ -+rake notes+ will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is only done in files with extension +.builder+, +.rb+, +.rxml+, +.rhtml+ and +.erb+ for both default and custom annotations. ++rake notes+ will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is done in files with extension +.builder+, +.rb+, +.erb+, +.haml+ and +.slim+ for both default and custom annotations. <shell> $ rake notes @@ -507,8 +509,8 @@ $ rails new . --git --database=postgresql create tmp/pids create Rakefile add 'Rakefile' - create README -add 'README' + create README.rdoc +add 'README.rdoc' create app/controllers/application_controller.rb add 'app/controllers/application_controller.rb' create app/helpers/application_helper.rb diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index 809948b41e..09ef308665 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -82,12 +82,18 @@ NOTE. The +config.asset_path+ configuration is ignored if the asset pipeline is * +config.encoding+ sets up the application-wide encoding. Defaults to UTF-8. +* +config.exceptions_app+ sets the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to +ActionDispatch::PublicExceptions.new(Rails.public_path)+. + +* +config.file_watcher+ the class used to detect file updates in the filesystem when +config.reload_classes_only_on_change+ is true. Must conform to +ActiveSupport::FileUpdateChecker+ API. + * +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.force_ssl+ forces all requests to be under HTTPS protocol by using +Rack::SSL+ middleware. * +config.log_level+ defines the verbosity of the Rails logger. This option defaults to +:debug+ for all modes except production, where it defaults to +:info+. +* +config.log_tags+ accepts a list of methods that respond to +request+ object. This makes it easy to tag log lines with debug information like subdomain and request id -- both very helpful in debugging multi-user production applications. + * +config.logger+ accepts a logger conforming to the interface of Log4r or the default Ruby +Logger+ class. Defaults to an instance of +ActiveSupport::BufferedLogger+, with auto flushing off in production mode. * +config.middleware+ allows you to configure the application's middleware. This is covered in depth in the "Configuring Middleware":#configuring-middleware section below. @@ -96,6 +102,8 @@ NOTE. The +config.asset_path+ configuration is ignored if the asset pipeline is * +config.preload_frameworks+ enables or disables preloading all frameworks at startup. Enabled by +config.threadsafe!+. Defaults to +nil+, so is disabled. +* +config.reload_classes_only_on_change+ enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If +config.cache_classes+ is true, this option is ignored. + * +config.reload_plugins+ enables or disables plugin reloading. Defaults to false. * +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. Applications get +config.secret_token+ initialized to a random key in +config/initializers/secret_token.rb+. @@ -188,6 +196,7 @@ Every Rails application comes with a standard set of middleware which it uses in * +Rack::Runtime+ sets an +X-Runtime+ header, containing the time (in seconds) taken to execute the request. * +Rails::Rack::Logger+ notifies 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::RequestId+ makes a unique X-Request-Id header available to the response and enables the +ActionDispatch::Request#uuid+ method. * +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+ 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.x_sendfile_header+. * +ActionDispatch::Callbacks+ runs the prepare callbacks before serving the request. @@ -265,6 +274,8 @@ h4. Configuring Active Record * +config.active_record.identity_map+ controls whether the identity map is enabled, and is false by default. +* +config.active_record.auto_explain_threshold_in_seconds+ configures the threshold for automatic EXPLAINs (+nil+ disables this feature). Queries exceeding the threshold get their query plan logged. Default is 0.5 in development mode. + The MySQL adapter adds one additional configuration option: * +ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans+ controls whether Active Record will consider all +tinyint(1)+ columns in a MySQL database to be booleans and is true by default. diff --git a/railties/guides/source/contributing_to_ruby_on_rails.textile b/railties/guides/source/contributing_to_ruby_on_rails.textile index 37ead2bff2..92cb0774de 100644 --- a/railties/guides/source/contributing_to_ruby_on_rails.textile +++ b/railties/guides/source/contributing_to_ruby_on_rails.textile @@ -336,7 +336,7 @@ It’s pretty likely that other changes to master have happened while you were w <shell> $ git checkout master -$ git pull +$ git pull --rebase </shell> Now reapply your patch on top of the latest changes: diff --git a/railties/guides/source/debugging_rails_applications.textile b/railties/guides/source/debugging_rails_applications.textile index 3552c68418..57c7786636 100644 --- a/railties/guides/source/debugging_rails_applications.textile +++ b/railties/guides/source/debugging_rails_applications.textile @@ -465,7 +465,7 @@ Now you should know where you are in the running trace and be able to print the Use +step+ (abbreviated +s+) to continue running your program until the next logical stopping point and return control to ruby-debug. -TIP: You can also use +step+ _n_+ and +step- _n_+ to move forward or backward _n_ steps respectively. +TIP: You can also use <tt>step<plus> n</tt> and <tt>step- n</tt> to move forward or backward +n+ steps respectively. You may also use +next+ which is similar to step, but function or method calls that appear within the line of code are executed without stopping. As with step, you may use plus sign to move _n_ steps. diff --git a/railties/guides/source/form_helpers.textile b/railties/guides/source/form_helpers.textile index 821bb305f6..64eb2d8f36 100644 --- a/railties/guides/source/form_helpers.textile +++ b/railties/guides/source/form_helpers.textile @@ -229,7 +229,7 @@ The corresponding view +app/views/articles/new.html.erb+ using +form_for+ looks There are a few things to note here: # +@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. +# There is a single hash of options. Routing options are passed in the +:url+ hash, HTML options are passed in the +:html+ hash. Also you can provide a +:namespace+ option for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id. # 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+ diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile index ca6a404212..774c6a792e 100644 --- a/railties/guides/source/getting_started.textile +++ b/railties/guides/source/getting_started.textile @@ -248,7 +248,7 @@ the following: $ rails --version </shell> -If it says something like "Rails 3.1.1" you are ready to continue. +If it says something like "Rails 3.1.3" you are ready to continue. h4. Creating the Blog Application @@ -289,7 +289,7 @@ rundown on the function of each of the files and folders that Rails created by d |log/|Application log files.| |public/|The only folder seen to the world as-is. Contains the static files and compiled assets.| |Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.| -|README|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.| +|README.rdoc|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.| |script/|Contains the rails script that starts your app and can contain other scripts you use to deploy or run your application.| |test/|Unit tests, fixtures, and other test apparatus. These are covered in "Testing Rails Applications":testing.html| |tmp/|Temporary files| diff --git a/railties/guides/source/i18n.textile b/railties/guides/source/i18n.textile index e9477e84cf..16ad35f345 100644 --- a/railties/guides/source/i18n.textile +++ b/railties/guides/source/i18n.textile @@ -825,7 +825,7 @@ h5. Active Record Methods * +ActiveRecord::Errors#generate_message+ (which is used by Active Record validations but may also be used manually) uses +model_name.human+ and +human_attribute_name+ (see above). It also translates the error message and supports translations for inherited class names as explained above in "Error message scopes". -*+ ActiveRecord::Errors#full_messages+ prepends the attribute name to the error message using a separator that will be looked up from "activerecord.errors.format.separator":https://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L91 (and which defaults to +' '+). +* +ActiveRecord::Errors#full_messages+ prepends the attribute name to the error message using a separator that will be looked up from "activerecord.errors.format.separator":https://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L91 (and which defaults to +' '+). h5. Active Support Methods diff --git a/railties/guides/source/layouts_and_rendering.textile b/railties/guides/source/layouts_and_rendering.textile index df7b9b364c..5cff2d0893 100644 --- a/railties/guides/source/layouts_and_rendering.textile +++ b/railties/guides/source/layouts_and_rendering.textile @@ -495,7 +495,7 @@ def show end </ruby> -Make sure to use +and return+ and not +&& return+, since +&& return+ will not work due to the operator precedence in the Ruby Language. +Make sure to use +and return+ instead of +&& return+ because +&& return+ will not work due to the operator precedence in the Ruby Language. Note that the implicit render done by ActionController detects if +render+ has been called, so the following will work without errors: @@ -671,19 +671,33 @@ There are three tag options available for the +auto_discovery_link_tag+: h5. Linking to JavaScript Files with the +javascript_include_tag+ -The +javascript_include_tag+ helper returns an HTML +script+ tag for each source provided. Rails looks in +public/javascripts+ for these files by default, but you can specify a full path relative to the document root, or a URL, if you prefer. For example, to include +public/javascripts/main.js+: +The +javascript_include_tag+ helper returns an HTML +script+ tag for each source provided. + +If you are using Rails with the "Asset Pipeline":asset_pipeline.html enabled, this helper will generate a link to +/assets/javascripts/+ rather than +public/javascripts+ which was used in earlier versions of Rails. This link is then served by the Sprockets gem, which was introduced in Rails 3.1. + +A JavaScript file within a Rails application or Rails engine goes in one of three locations: +app/assets+, +lib/assets+ or +vendor/assets+. These locations are explained in detail in the "Asset Organization section in the Asset Pipeline Guide":asset_pipeline.html#asset-organization + +You can specify a full path relative to the document root, or a URL, if you prefer. For example, to link to a JavaScript file that is inside a directory called +javascripts+ inside of one of +app/assets+, +lib/assets+ or +vendor/assets+, you would do this: <erb> <%= javascript_include_tag "main" %> </erb> -To include +public/javascripts/main.js+ and +public/javascripts/columns.js+: +Rails will then output a +script+ tag such as this: + +<html> +<script src='/assets/main.js' type="text/javascript"></script> +</html> + +The request to this asset is then served by the Sprockets gem. + +To include multiple files such as +app/assets/javascripts/main.js+ and +app/assets/javascripts/columns.js+ at the same time: <erb> <%= javascript_include_tag "main", "columns" %> </erb> -To include +public/javascripts/main.js+ and +public/photos/columns.js+: +To include +app/assets/javascripts/main.js+ and +app/assets/javascripts/photos/columns.js+: <erb> <%= javascript_include_tag "main", "/photos/columns" %> @@ -701,15 +715,38 @@ If the application does not use the asset pipeline, the +:defaults+ option loads <%= javascript_include_tag :defaults %> </erb> -And you can in any case override the expansion in <tt>config/application.rb</tt>: +Outputting +script+ tags such as this: + +<html> +<script src="/javascripts/jquery.js" type="text/javascript"></script> +<script src="/javascripts/jquery_ujs.js" type="text/javascript"></script> +</html> + +These two files for jQuery, +jquery.js+ and +jquery_ujs.js+ must be placed inside +public/javascripts+ if the application doesn't use the asset pipeline. These files can be downloaded from the "jquery-rails repository on GitHub":https://github.com/indirect/jquery-rails/tree/master/vendor/assets/javascripts + +WARNING: If you are using the asset pipeline, this tag will render a +script+ tag for an asset called +defaults.js+, which would not exist in your application unless you've explicitly defined it to be. + +And you can in any case override the +:defaults+ expansion in <tt>config/application.rb</tt>: <ruby> config.action_view.javascript_expansions[:defaults] = %w(foo.js bar.js) </ruby> -When using <tt>:defaults</tt>, if an <tt>application.js</tt> file exists in <tt>public/javascripts</tt> it will be included as well at then end. +You can also define new defaults: + +<ruby> +config.action_view.javascript_expansions[:projects] = %w(projects.js tickets.js) +</ruby> + +And use them by referencing them exactly like +:defaults+: -Also, the +:all+ option loads every JavaScript file in +public/javascripts+: +<erb> +<%= javascript_include_tag :projects %> +</erb> + +When using <tt>:defaults</tt>, if an <tt>application.js</tt> file exists in <tt>public/javascripts</tt> it will be included as well at the end. + +Also, if the asset pipeline is disabled, the +:all+ expansion loads every JavaScript file in +public/javascripts+: <erb> <%= javascript_include_tag :all %> @@ -740,19 +777,23 @@ You can even use dynamic paths such as +cache/#{current_site}/main/display+. h5. Linking to CSS Files with the +stylesheet_link_tag+ -The +stylesheet_link_tag+ helper returns an HTML +<link>+ tag for each source provided. Rails looks in +public/stylesheets+ for these files by default, but you can specify a full path relative to the document root, or a URL, if you prefer. For example, to include +public/stylesheets/main.css+: +The +stylesheet_link_tag+ helper returns an HTML +<link>+ tag for each source provided. + +If you are using Rails with the "Asset Pipeline" enabled, this helper will generate a link to +/assets/stylesheets/+. This link is then processed by the Sprockets gem. A stylesheet file can be stored in one of three locations: +app/assets+, +lib/assets+ or +vendor/assets+. + +You can specify a full path relative to the document root, or a URL. For example, to link to a stylesheet file that is inside a directory called +stylesheets+ inside of one of +app/assets+, +lib/assets+ or +vendor/assets+, you would do this: <erb> <%= stylesheet_link_tag "main" %> </erb> -To include +public/stylesheets/main.css+ and +public/stylesheets/columns.css+: +To include +app/assets/stylesheets/main.css+ and +app/assets/stylesheets/columns.css+: <erb> <%= stylesheet_link_tag "main", "columns" %> </erb> -To include +public/stylesheets/main.css+ and +public/photos/columns.css+: +To include +app/assets/stylesheets/main.css+ and +app/assets/stylesheets/photos/columns.css+: <erb> <%= stylesheet_link_tag "main", "/photos/columns" %> @@ -770,7 +811,7 @@ By default, the +stylesheet_link_tag+ creates links with +media="screen" rel="st <%= stylesheet_link_tag "main_print", :media => "print" %> </erb> -The +all+ option links every CSS file in +public/stylesheets+: +If the asset pipeline is disabled, the +all+ option links every CSS file in +public/stylesheets+: <erb> <%= stylesheet_link_tag :all %> diff --git a/railties/guides/source/migrations.textile b/railties/guides/source/migrations.textile index 5b52a93853..92356edf90 100644 --- a/railties/guides/source/migrations.textile +++ b/railties/guides/source/migrations.textile @@ -1,12 +1,24 @@ h2. Migrations -Migrations are a convenient way for you to alter your database in a structured and organized manner. You could edit fragments of SQL by hand but you would then be responsible for telling other developers that they need to go and run them. You'd also have to keep track of which changes need to be run against the production machines next time you deploy. - -Active Record tracks which migrations have already been run so all you have to do is update your source and run +rake db:migrate+. Active Record will work out which migrations should be run. It will also update your +db/schema.rb+ file to match the structure of your database. - -Migrations also allow you to describe these transformations using Ruby. The great thing about this is that (like most of Active Record's functionality) it is database independent: you don't need to worry about the precise syntax of +CREATE TABLE+ any more than you worry about variations on +SELECT *+ (you can drop down to raw SQL for database specific features). For example you could use SQLite3 in development, but MySQL in production. - -You'll learn all about migrations including: +Migrations are a convenient way for you to alter your database in a structured +and organized manner. You could edit fragments of SQL by hand but you would then +be responsible for telling other developers that they need to go and run them. +You'd also have to keep track of which changes need to be run against the +production machines next time you deploy. + +Active Record tracks which migrations have already been run so all you have to +do is update your source and run +rake db:migrate+. Active Record will work out +which migrations should be run. It will also update your +db/schema.rb+ file to +match the structure of your database. + +Migrations also allow you to describe these transformations using Ruby. The +great thing about this is that (like most of Active Record's functionality) it +is database independent: you don't need to worry about the precise syntax of ++CREATE TABLE+ any more than you worry about variations on +SELECT *+ (you can +drop down to raw SQL for database specific features). For example you could use +SQLite3 in development, but MySQL in production. + +In this guide, you'll learn all about migrations including: * The generators you can use to create them * The methods Active Record provides to manipulate your database @@ -17,7 +29,8 @@ endprologue. h3. Anatomy of a Migration -Before we dive into the details of a migration, here are a few examples of the sorts of things you can do: +Before we dive into the details of a migration, here are a few examples of the +sorts of things you can do: <ruby> class CreateProducts < ActiveRecord::Migration @@ -36,9 +49,15 @@ class CreateProducts < ActiveRecord::Migration end </ruby> -This migration adds a table called +products+ with a string column called +name+ and a text column called +description+. A primary key column called +id+ will also be added, however since this is the default we do not need to ask for this. The timestamp columns +created_at+ and +updated_at+ which Active Record populates automatically will also be added. Reversing this migration is as simple as dropping the table. +This migration adds a table called +products+ with a string column called +name+ +and a text column called +description+. A primary key column called +id+ will +also be added, however since this is the default we do not need to ask for this. +The timestamp columns +created_at+ and +updated_at+ which Active Record +populates automatically will also be added. Reversing this migration is as +simple as dropping the table. -Migrations are not limited to changing the schema. You can also use them to fix bad data in the database or populate new fields: +Migrations are not limited to changing the schema. You can also use them to fix +bad data in the database or populate new fields: <ruby> class AddReceiveNewsletterToUsers < ActiveRecord::Migration @@ -55,12 +74,18 @@ class AddReceiveNewsletterToUsers < ActiveRecord::Migration end </ruby> -NOTE: Some "caveats":#using-models-in-your-migrations apply to using models in your migrations. +NOTE: Some "caveats":#using-models-in-your-migrations apply to using models in +your migrations. -This migration adds a +receive_newsletter+ column to the +users+ table. We want it to default to +false+ for new users, but existing users are considered -to have already opted in, so we use the User model to set the flag to +true+ for existing users. +This migration adds a +receive_newsletter+ column to the +users+ table. We want +it to default to +false+ for new users, but existing users are considered to +have already opted in, so we use the User model to set the flag to +true+ for +existing users. -Rails 3.1 makes migrations smarter by providing a new <tt>change</tt> method. This method is preferred for writing constructive migrations (adding columns or tables). The migration knows how to migrate your database and reverse it when the migration is rolled back without the need to write a separate +down+ method. +Rails 3.1 makes migrations smarter by providing a new <tt>change</tt> method. +This method is preferred for writing constructive migrations (adding columns or +tables). The migration knows how to migrate your database and reverse it when +the migration is rolled back without the need to write a separate +down+ method. <ruby> class CreateProducts < ActiveRecord::Migration @@ -77,64 +102,111 @@ end h4. Migrations are Classes -A migration is a subclass of <tt>ActiveRecord::Migration</tt> that implements two methods: +up+ (perform the required transformations) and +down+ (revert them). +A migration is a subclass of <tt>ActiveRecord::Migration</tt> that implements +two methods: +up+ (perform the required transformations) and +down+ (revert +them). -Active Record provides methods that perform common data definition tasks in a database independent way (you'll read about them in detail later): +Active Record provides methods that perform common data definition tasks in a +database independent way (you'll read about them in detail later): -* +create_table+ -* +change_table+ -* +drop_table+ * +add_column+ +* +add_index+ * +change_column+ -* +rename_column+ +* +change_table+ +* +create_table+ +* +drop_table+ * +remove_column+ -* +add_index+ * +remove_index+ +* +rename_column+ -If you need to perform tasks specific to your database (for example create a "foreign key":#active-record-and-referential-integrity constraint) then the +execute+ method allows you to execute arbitrary SQL. A migration is just a regular Ruby class so you're not limited to these functions. For example after adding a column you could write code to set the value of that column for existing records (if necessary using your models). +If you need to perform tasks specific to your database (for example create a +"foreign key":#active-record-and-referential-integrity constraint) then the ++execute+ method allows you to execute arbitrary SQL. A migration is just a +regular Ruby class so you're not limited to these functions. For example after +adding a column you could write code to set the value of that column for +existing records (if necessary using your models). -On databases that support transactions with statements that change the schema (such as PostgreSQL or SQLite3), migrations are wrapped in a transaction. If the database does not support this (for example MySQL) then when a migration fails the parts of it that succeeded will not be rolled back. You will have to unpick the changes that were made by hand. +On databases that support transactions with statements that change the schema +(such as PostgreSQL or SQLite3), migrations are wrapped in a transaction. If the +database does not support this (for example MySQL) then when a migration fails +the parts of it that succeeded will not be rolled back. You will have to rollback +the changes that were made by hand. h4. What's in a Name -Migrations are stored in files in +db/migrate+, one for each migration class. The name of the file is of the form +YYYYMMDDHHMMSS_create_products.rb+, that is to say a UTC timestamp identifying the migration followed by an underscore followed by the name of the migration. The name of the migration class (CamelCased version) should match the latter part of the file name. For example +20080906120000_create_products.rb+ should define class +CreateProducts+ and +20080906120001_add_details_to_products.rb+ should define +AddDetailsToProducts+. If you do feel the need to change the file name then you <em>have to</em> update the name of the class inside or Rails will complain about a missing class. - -Internally Rails only uses the migration's number (the timestamp) to identify them. Prior to Rails 2.1 the migration number started at 1 and was incremented each time a migration was generated. With multiple developers it was easy for these to clash requiring you to rollback migrations and renumber them. With Rails 2.1 this is largely avoided by using the creation time of the migration to identify them. You can revert to the old numbering scheme by adding the following line to +config/application.rb+. +Migrations are stored as files in the +db/migrate+ directory, one for each +migration class. The name of the file is of the form ++YYYYMMDDHHMMSS_create_products.rb+, that is to say a UTC timestamp +identifying the migration followed by an underscore followed by the name +of the migration. The name of the migration class (CamelCased version) +should match the latter part of the file name. For example ++20080906120000_create_products.rb+ should define class +CreateProducts+ and ++20080906120001_add_details_to_products.rb+ should define ++AddDetailsToProducts+. If you do feel the need to change the file name then you +<em>have to</em> update the name of the class inside or Rails will complain +about a missing class. + +Internally Rails only uses the migration's number (the timestamp) to identify +them. Prior to Rails 2.1 the migration number started at 1 and was incremented +each time a migration was generated. With multiple developers it was easy for +these to clash requiring you to rollback migrations and renumber them. With +Rails 2.1+ this is largely avoided by using the creation time of the migration +to identify them. You can revert to the old numbering scheme by adding the +following line to +config/application.rb+. <ruby> config.active_record.timestamped_migrations = false </ruby> -The combination of timestamps and recording which migrations have been run allows Rails to handle common situations that occur with multiple developers. +The combination of timestamps and recording which migrations have been run +allows Rails to handle common situations that occur with multiple developers. -For example Alice adds migrations +20080906120000+ and +20080906123000+ and Bob adds +20080906124500+ and runs it. Alice finishes her changes and checks in her migrations and Bob pulls down the latest changes. Rails knows that it has not run Alice's two migrations so +rake db:migrate+ would run them (even though Bob's migration with a later timestamp has been run), and similarly migrating down would not run their +down+ methods. +For example Alice adds migrations +20080906120000+ and +20080906123000+ and Bob +adds +20080906124500+ and runs it. Alice finishes her changes and checks in her +migrations and Bob pulls down the latest changes. When Bob runs +rake db:migrate+, +Rails knows that it has not run Alice's two migrations so it executes the +up+ method for each migration. -Of course this is no substitution for communication within the team. For example, if Alice's migration removed a table that Bob's migration assumed to exist, then trouble would certainly strike. +Of course this is no substitution for communication within the team. For +example, if Alice's migration removed a table that Bob's migration assumed to +exist, then trouble would certainly strike. h4. Changing Migrations -Occasionally you will make a mistake when writing a migration. If you have already run the migration then you cannot just edit the migration and run the migration again: Rails thinks it has already run the migration and so will do nothing when you run +rake db:migrate+. You must rollback the migration (for example with +rake db:rollback+), edit your migration and then run +rake db:migrate+ to run the corrected version. +Occasionally you will make a mistake when writing a migration. If you have +already run the migration then you cannot just edit the migration and run the +migration again: Rails thinks it has already run the migration and so will do +nothing when you run +rake db:migrate+. You must rollback the migration (for +example with +rake db:rollback+), edit your migration and then run +rake db:migrate+ to run the corrected version. -In general editing existing migrations is not a good idea: you will be creating extra work for yourself and your co-workers and cause major headaches if the existing version of the migration has already been run on production machines. Instead, you should write a new migration that performs the changes you require. Editing a freshly generated migration that has not yet been committed to source control (or, more generally, which has not been propagated beyond your development machine) is relatively harmless. +In general editing existing migrations is not a good idea: you will be creating +extra work for yourself and your co-workers and cause major headaches if the +existing version of the migration has already been run on production machines. +Instead, you should write a new migration that performs the changes you require. +Editing a freshly generated migration that has not yet been committed to source +control (or, more generally, which has not been propagated beyond your +development machine) is relatively harmless. h4. Supported Types -Active Record supports the following types: +Active Record supports the following database column types: +* +:binary+ +* +:boolean+ +* +:date+ +* +:datetime+ +* +:decimal+ +* +:float+ +* +:integer+ * +:primary_key+ * +:string+ * +:text+ -* +:integer+ -* +:float+ -* +:decimal+ -* +:datetime+ -* +:timestamp+ * +:time+ -* +:date+ -* +:binary+ -* +:boolean+ +* +:timestamp+ -These will be mapped onto an appropriate underlying database type. For example, with MySQL the type +:string+ is mapped to +VARCHAR(255)+. You can create columns of types not supported by Active Record when using the non-sexy syntax, for example +These will be mapped onto an appropriate underlying database type. For example, +with MySQL the type +:string+ is mapped to +VARCHAR(255)+. You can create +columns of types not supported by Active Record when using the non-sexy syntax, +for example <ruby> create_table :products do |t| @@ -148,7 +220,10 @@ h3. Creating a Migration h4. Creating a Model -The model and scaffold generators will create migrations appropriate for adding a new model. This migration will already contain instructions for creating the relevant table. If you tell Rails what columns you want, then statements for adding these columns will also be created. For example, running +The model and scaffold generators will create migrations appropriate for adding +a new model. This migration will already contain instructions for creating the +relevant table. If you tell Rails what columns you want, then statements for +adding these columns will also be created. For example, running <shell> $ rails generate model Product name:string description:text @@ -169,12 +244,15 @@ class CreateProducts < ActiveRecord::Migration end </ruby> -You can append as many column name/type pairs as you want. By default +t.timestamps+ (which creates the +updated_at+ and +created_at+ columns that -are automatically populated by Active Record) will be added for you. +You can append as many column name/type pairs as you want. By default, the +generated migration will include +t.timestamps+ (which creates the ++updated_at+ and +created_at+ columns that are automatically populated +by Active Record). h4. Creating a Standalone Migration -If you are creating migrations for other purposes (for example to add a column to an existing table) then you can use the migration generator: +If you are creating migrations for other purposes (for example to add a column +to an existing table) then you can also use the migration generator: <shell> $ rails generate migration AddPartNumberToProducts @@ -189,7 +267,9 @@ class AddPartNumberToProducts < ActiveRecord::Migration end </ruby> -If the migration name is of the form "AddXXXToYYY" or "RemoveXXXFromYYY" and is followed by a list of column names and types then a migration containing the appropriate +add_column+ and +remove_column+ statements will be created. +If the migration name is of the form "AddXXXToYYY" or "RemoveXXXFromYYY" and is +followed by a list of column names and types then a migration containing the +appropriate +add_column+ and +remove_column+ statements will be created. <shell> $ rails generate migration AddPartNumberToProducts part_number:string @@ -242,17 +322,23 @@ class AddDetailsToProducts < ActiveRecord::Migration end </ruby> -As always, what has been generated for you is just a starting point. You can add or remove from it as you see fit. +As always, what has been generated for you is just a starting point. You can add +or remove from it as you see fit by editing the +db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb file. -NOTE: The generated migration file for destructive migrations will still be old-style using the +up+ and +down+ methods. This is because Rails doesn't know the original data types defined when you made the original changes. +NOTE: The generated migration file for destructive migrations will still be +old-style using the +up+ and +down+ methods. This is because Rails needs to know +the original data types defined when you made the original changes. h3. Writing a Migration -Once you have created your migration using one of the generators it's time to get to work! +Once you have created your migration using one of the generators it's time to +get to work! h4. Creating a Table -Migration method +create_table+ will be one of your workhorses. A typical use would be +Migration method +create_table+ will be one of your workhorses. A typical use +would be <ruby> create_table :products do |t| @@ -260,9 +346,11 @@ create_table :products do |t| end </ruby> -which creates a +products+ table with a column called +name+ (and as discussed below, an implicit +id+ column). +which creates a +products+ table with a column called +name+ (and as discussed +below, an implicit +id+ column). -The object yielded to the block allows you to create columns on the table. There are two ways of doing it. The first (traditional) form looks like +The object yielded to the block allows you to create columns on the table. There +are two ways of doing it. The first (traditional) form looks like <ruby> create_table :products do |t| @@ -270,7 +358,9 @@ create_table :products do |t| end </ruby> -The second form, the so called "sexy" migration, drops the somewhat redundant +column+ method. Instead, the +string+, +integer+, etc. methods create a column of that type. Subsequent parameters are the same. +The second form, the so called "sexy" migration, drops the somewhat redundant ++column+ method. Instead, the +string+, +integer+, etc. methods create a column +of that type. Subsequent parameters are the same. <ruby> create_table :products do |t| @@ -278,7 +368,12 @@ create_table :products do |t| end </ruby> -By default, +create_table+ will create a primary key called +id+. You can change the name of the primary key with the +:primary_key+ option (don't forget to update the corresponding model) or, if you don't want a primary key at all (for example for a HABTM join table), you can pass the option +:id => false+. If you need to pass database specific options you can place an SQL fragment in the +:options+ option. For example, +By default, +create_table+ will create a primary key called +id+. You can change +the name of the primary key with the +:primary_key+ option (don't forget to +update the corresponding model) or, if you don't want a primary key at all (for +example for a HABTM join table), you can pass the option +:id => false+. If you +need to pass database specific options you can place an SQL fragment in the ++:options+ option. For example, <ruby> create_table :products, :options => "ENGINE=BLACKHOLE" do |t| @@ -286,11 +381,14 @@ create_table :products, :options => "ENGINE=BLACKHOLE" do |t| end </ruby> -will append +ENGINE=BLACKHOLE+ to the SQL statement used to create the table (when using MySQL, the default is +ENGINE=InnoDB+). +will append +ENGINE=BLACKHOLE+ to the SQL statement used to create the table +(when using MySQL, the default is +ENGINE=InnoDB+). h4. Changing Tables -A close cousin of +create_table+ is +change_table+, used for changing existing tables. It is used in a similar fashion to +create_table+ but the object yielded to the block knows more tricks. For example +A close cousin of +create_table+ is +change_table+, used for changing existing +tables. It is used in a similar fashion to +create_table+ but the object yielded +to the block knows more tricks. For example <ruby> change_table :products do |t| @@ -301,28 +399,23 @@ change_table :products do |t| end </ruby> -removes the +description+ and +name+ columns, creates a +part_number+ column and adds an index on it. Finally it renames the +upccode+ column. This is the same as doing - -<ruby> -remove_column :products, :description -remove_column :products, :name -add_column :products, :part_number, :string -add_index :products, :part_number -rename_column :products, :upccode, :upc_code -</ruby> - -You don't have to keep repeating the table name and it groups all the statements related to modifying one particular table. The individual transformation names are also shorter, for example +remove_column+ becomes just +remove+ and +add_index+ becomes just +index+. +removes the +description+ and +name+ columns, creates a +part_number+ string +column and adds an index on it. Finally it renames the +upccode+ column. h4. Special Helpers -Active Record provides some shortcuts for common functionality. It is for example very common to add both the +created_at+ and +updated_at+ columns and so there is a method that does exactly that: +Active Record provides some shortcuts for common functionality. It is for +example very common to add both the +created_at+ and +updated_at+ columns and so +there is a method that does exactly that: <ruby> create_table :products do |t| t.timestamps end </ruby> -will create a new products table with those two columns (plus the +id+ column) whereas + +will create a new products table with those two columns (plus the +id+ column) +whereas <ruby> change_table :products do |t| @@ -331,7 +424,8 @@ end </ruby> adds those columns to an existing table. -The other helper is called +references+ (also available as +belongs_to+). In its simplest form it just adds some readability +Another helper is called +references+ (also available as +belongs_to+). In its +simplest form it just adds some readability. <ruby> create_table :products do |t| @@ -339,24 +433,42 @@ create_table :products do |t| end </ruby> -will create a +category_id+ column of the appropriate type. Note that you pass the model name, not the column name. Active Record adds the +_id+ for you. If you have polymorphic +belongs_to+ associations then +references+ will add both of the columns required: +will create a +category_id+ column of the appropriate type. Note that you pass +the model name, not the column name. Active Record adds the +_id+ for you. If +you have polymorphic +belongs_to+ associations then +references+ will add both +of the columns required: <ruby> create_table :products do |t| t.references :attachment, :polymorphic => {:default => 'Photo'} end </ruby> -will add an +attachment_id+ column and a string +attachment_type+ column with a default value of 'Photo'. -NOTE: The +references+ helper does not actually create foreign key constraints for you. You will need to use +execute+ or a plugin that adds "foreign key support":#active-record-and-referential-integrity. +will add an +attachment_id+ column and a string +attachment_type+ column with +a default value of 'Photo'. + +NOTE: The +references+ helper does not actually create foreign key constraints +for you. You will need to use +execute+ or a plugin that adds "foreign key +support":#active-record-and-referential-integrity. -If the helpers provided by Active Record aren't enough you can use the +execute+ method to execute arbitrary SQL. +If the helpers provided by Active Record aren't enough you can use the +execute+ +method to execute arbitrary SQL. -For more details and examples of individual methods, check the API documentation, in particular the documentation for "<tt>ActiveRecord::ConnectionAdapters::SchemaStatements</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html (which provides the methods available in the +up+ and +down+ methods), "<tt>ActiveRecord::ConnectionAdapters::TableDefinition</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html (which provides the methods available on the object yielded by +create_table+) and "<tt>ActiveRecord::ConnectionAdapters::Table</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html (which provides the methods available on the object yielded by +change_table+). +For more details and examples of individual methods, check the API documentation, +in particular the documentation for +"<tt>ActiveRecord::ConnectionAdapters::SchemaStatements</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html +(which provides the methods available in the +up+ and +down+ methods), +"<tt>ActiveRecord::ConnectionAdapters::TableDefinition</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html +(which provides the methods available on the object yielded by +create_table+) +and +"<tt>ActiveRecord::ConnectionAdapters::Table</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html +(which provides the methods available on the object yielded by +change_table+). -h4. Writing Your +change+ Method +h4. Using the +change+ Method -The +change+ method removes the need to write both +up+ and +down+ methods in those cases that Rails know how to revert the changes automatically. Currently, the +change+ method supports only these migration definitions: +The +change+ method removes the need to write both +up+ and +down+ methods in +those cases that Rails know how to revert the changes automatically. Currently, +the +change+ method supports only these migration definitions: * +add_column+ * +add_index+ @@ -367,15 +479,20 @@ The +change+ method removes the need to write both +up+ and +down+ methods in th * +rename_index+ * +rename_table+ -If you're going to use other methods, you'll have to write the +up+ and +down+ methods normally. +If you're going to need to use any other methods, you'll have to write the ++up+ and +down+ methods instead of using the +change+ method. -h4. Writing Your +down+ Method +h4. Using the +up+/+down+ Methods -The +down+ method of your migration should revert the transformations done by the +up+ method. In other words, the database schema should be unchanged if you do an +up+ followed by a +down+. For example, if you create a table in the +up+ method, you should drop it in the +down+ method. It is wise to reverse the transformations in precisely the reverse order they were made in the +up+ method. For example, +The +down+ method of your migration should revert the transformations done by +the +up+ method. In other words, the database schema should be unchanged if you +do an +up+ followed by a +down+. For example, if you create a table in the +up+ +method, you should drop it in the +down+ method. It is wise to reverse the +transformations in precisely the reverse order they were made in the +up+ +method. For example, <ruby> class ExampleMigration < ActiveRecord::Migration - def up create_table :products do |t| t.references :category @@ -387,47 +504,69 @@ class ExampleMigration < ActiveRecord::Migration FOREIGN KEY (category_id) REFERENCES categories(id) SQL - add_column :users, :home_page_url, :string - rename_column :users, :email, :email_address end def down rename_column :users, :email_address, :email remove_column :users, :home_page_url - execute "ALTER TABLE products DROP FOREIGN KEY fk_products_categories" + execute <<-SQL + ALTER TABLE products + DROP FOREIGN KEY fk_products_categories + SQL drop_table :products end end </ruby> -Sometimes your migration will do something which is just plain irreversible; for example, it might destroy some data. In such cases, you can raise +ActiveRecord::IrreversibleMigration+ from your +down+ method. If someone tries to revert your migration, an error message will be displayed saying that it can't be done. +Sometimes your migration will do something which is just plain irreversible; for +example, it might destroy some data. In such cases, you can raise ++ActiveRecord::IrreversibleMigration+ from your +down+ method. If someone tries +to revert your migration, an error message will be displayed saying that it +can't be done. h3. Running Migrations -Rails provides a set of rake tasks to work with migrations which boil down to running certain sets of migrations. The very first migration related rake task you will use will probably be +db:migrate+. In its most basic form it just runs the +up+ method for all the migrations that have not yet been run. If there are no such migrations, it exits. +Rails provides a set of rake tasks to work with migrations which boil down to +running certain sets of migrations. + +The very first migration related rake task you will use will probably be ++rake db:migrate+. In its most basic form it just runs the +up+ or +change+ +method for all the migrations that have not yet been run. If there are +no such migrations, it exits. It will run these migrations in order based +on the date of the migration. -Note that running the +db:migrate+ also invokes the +db:schema:dump+ task, which will update your db/schema.rb file to match the structure of your database. +Note that running the +db:migrate+ also invokes the +db:schema:dump+ task, which +will update your db/schema.rb file to match the structure of your database. -If you specify a target version, Active Record will run the required migrations (up or down) until it has reached the specified version. The -version is the numerical prefix on the migration's filename. For example, to migrate to version 20080906120000 run +If you specify a target version, Active Record will run the required migrations +(up, down or change) until it has reached the specified version. The version +is the numerical prefix on the migration's filename. For example, to migrate +to version 20080906120000 run <shell> $ rake db:migrate VERSION=20080906120000 </shell> -If version 20080906120000 is greater than the current version (i.e., it is migrating upwards), this will run the +up+ method on all migrations up to and including 20080906120000. If migrating downwards, this will run the +down+ method on all the migrations down to, but not including, 20080906120000. +If version 20080906120000 is greater than the current version (i.e., it is +migrating upwards), this will run the +up+ method on all migrations up to and +including 20080906120000, and will not execute any later migrations. If +migrating downwards, this will run the +down+ method on all the migrations +down to, but not including, 20080906120000. h4. Rolling Back -A common task is to rollback the last migration, for example if you made a mistake in it and wish to correct it. Rather than tracking down the version number associated with the previous migration you can run +A common task is to rollback the last migration, for example if you made a +mistake in it and wish to correct it. Rather than tracking down the version +number associated with the previous migration you can run <shell> $ rake db:rollback </shell> -This will run the +down+ method from the latest migration. If you need to undo several migrations you can provide a +STEP+ parameter: +This will run the +down+ method from the latest migration. If you need to undo +several migrations you can provide a +STEP+ parameter: <shell> $ rake db:rollback STEP=3 @@ -435,46 +574,65 @@ $ rake db:rollback STEP=3 will run the +down+ method from the last 3 migrations. -The +db:migrate:redo+ task is a shortcut for doing a rollback and then migrating back up again. As with the +db:rollback+ task, you can use the +STEP+ parameter if you need to go more than one version back, for example +The +db:migrate:redo+ task is a shortcut for doing a rollback and then migrating +back up again. As with the +db:rollback+ task, you can use the +STEP+ parameter +if you need to go more than one version back, for example <shell> $ rake db:migrate:redo STEP=3 </shell> -Neither of these Rake tasks do anything you could not do with +db:migrate+. They are simply more convenient, since you do not need to explicitly specify the version to migrate to. +Neither of these Rake tasks do anything you could not do with +db:migrate+. They +are simply more convenient, since you do not need to explicitly specify the +version to migrate to. -Lastly, the +db:reset+ task will drop the database, recreate it and load the current schema into it. +h4. Resetting the database -NOTE: This is not the same as running all the migrations - see the section on "schema.rb":#schema-dumping-and-you. +The +rake db:reset+ task will drop the database, recreate it and load the +current schema into it. -h4. Being Specific +NOTE: This is not the same as running all the migrations - see the section on +"schema.rb":#schema-dumping-and-you. -If you need to run a specific migration up or down, the +db:migrate:up+ and +db:migrate:down+ tasks will do that. Just specify the appropriate version and the corresponding migration will have its +up+ or +down+ method invoked, for example, +h4. Running specific migrations + +If you need to run a specific migration up or down, the +db:migrate:up+ and ++db:migrate:down+ tasks will do that. Just specify the appropriate version and +the corresponding migration will have its +up+ or +down+ method invoked, for +example, <shell> $ rake db:migrate:up VERSION=20080906120000 </shell> -will run the +up+ method from the 20080906120000 migration. These tasks check whether the migration has already run, so for example +db:migrate:up VERSION=20080906120000+ will do nothing if Active Record believes that 20080906120000 has already been run. +will run the +up+ method from the 20080906120000 migration. These tasks still +check whether the migration has already run, so for example +db:migrate:up +VERSION=20080906120000+ will do nothing if Active Record believes that +20080906120000 has already been run. -h4. Being Talkative +h4. Changing the output of running migrations -By default migrations tell you exactly what they're doing and how long it took. A migration creating a table and adding an index might produce output like this +By default migrations tell you exactly what they're doing and how long it took. +A migration creating a table and adding an index might produce output like this <shell> -20080906170109 CreateProducts: migrating +== CreateProducts: migrating ================================================= -- create_table(:products) - -> 0.0021s --- add_index(:products, :name) - -> 0.0026s -20080906170109 CreateProducts: migrated (0.0059s) + -> 0.0028s +== CreateProducts: migrated (0.0028s) ======================================== </shell> -Several methods are provided that allow you to control all this: +Several methods are provided in migrations that allow you to control all this: -* +suppress_messages+ takes a block as an argument and suppresses any output generated by the block. -* +say+ takes a message argument and outputs it as is. A second boolean argument can be passed to specify whether to indent or not. -* +say_with_time+ outputs text along with how long it took to run its block. If the block returns an integer it assumes it is the number of rows affected. +|_.Method |_.Purpose| +|suppress_messages |Takes a block as an argument and suppresses any output + generated by the block.| +|say |Takes a message argument and outputs it as is. A second + boolean argument can be passed to specify whether to + indent or not.| +|say_with_time |Outputs text along with how long it took to run its + block. If the block returns an integer it assumes it + is the number of rows affected.| For example, this migration @@ -502,37 +660,46 @@ end generates the following output <shell> -20080906170109 CreateProducts: migrating - Created a table +== CreateProducts: migrating ================================================= +-- Created a table -> and an index! - Waiting for a while - -> 10.0001s +-- Waiting for a while + -> 10.0013s -> 250 rows -20080906170109 CreateProducts: migrated (10.0097s) +== CreateProducts: migrated (10.0054s) ======================================= </shell> -If you just want Active Record to shut up, then running +rake db:migrate VERBOSE=false+ will suppress all output. +If you want Active Record to not output anything, then running +rake db:migrate +VERBOSE=false+ will suppress all output. h3. Using Models in Your Migrations -When creating or updating data in a migration it is often tempting to use one of your models. After all, they exist to provide easy access to the underlying data. This can be done, but some caution should be observed. +When creating or updating data in a migration it is often tempting to use one of +your models. After all, they exist to provide easy access to the underlying +data. This can be done, but some caution should be observed. -For example, problems occur when the model uses database columns which are (1) not currently in the database and (2) will be created by this or a subsequent migration. +For example, problems occur when the model uses database columns which are (1) +not currently in the database and (2) will be created by this or a subsequent +migration. -Consider this example, where Alice and Bob are working on the same code base which contains a +Product+ model: +Consider this example, where Alice and Bob are working on the same code base +which contains a +Product+ model: Bob goes on vacation. -Alice creates a migration for the +products+ table which adds a new column and initializes it. -She also adds a validation to the +Product+ model for the new column. +Alice creates a migration for the +products+ table which adds a new column and +initializes it. She also adds a validation to the +Product+ model for the new +column. <ruby> # db/migrate/20100513121110_add_flag_to_product.rb class AddFlagToProduct < ActiveRecord::Migration def change - add_column :products, :flag, :int - Product.all.each { |f| f.update_attributes!(:flag => 'false') } + add_column :products, :flag, :boolean + Product.all.each do |product| + product.update_attributes!(:flag => 'false') + end end end </ruby> @@ -545,7 +712,9 @@ class Product < ActiveRecord::Base end </ruby> -Alice adds a second migration which adds and initializes another column to the +products+ table and also adds a validation to the +Product+ model for the new column. +Alice adds a second migration which adds and initializes another column to the ++products+ table and also adds a validation to the +Product+ model for the new +column. <ruby> # db/migrate/20100515121110_add_fuzz_to_product.rb @@ -553,7 +722,9 @@ Alice adds a second migration which adds and initializes another column to the + class AddFuzzToProduct < ActiveRecord::Migration def change add_column :products, :fuzz, :string - Product.all.each { |f| f.update_attributes! :fuzz => 'fuzzy' } + Product.all.each do |product| + product.update_attributes! :fuzz => 'fuzzy' + end end end </ruby> @@ -570,10 +741,14 @@ Both migrations work for Alice. Bob comes back from vacation and: -# updates the source - which contains both migrations and the latests version of the Product model. -# runs outstanding migrations with +rake db:migrate+, which includes the one that updates the +Product+ model. +# Updates the source - which contains both migrations and the latests version of +the Product model. +# Runs outstanding migrations with +rake db:migrate+, which +includes the one that updates the +Product+ model. -The migration crashes because when the model attempts to save, it tries to validate the second added column, which is not in the database when the _first_ migration runs: +The migration crashes because when the model attempts to save, it tries to +validate the second added column, which is not in the database when the _first_ +migration runs: <plain> rake aborted! @@ -582,9 +757,12 @@ An error has occurred, this and all later migrations canceled: undefined method `fuzz' for #<Product:0x000001049b14a0> </plain> -A fix for this is to create a local model within the migration. This keeps rails from running the validations, so that the migrations run to completion. +A fix for this is to create a local model within the migration. This keeps rails +from running the validations, so that the migrations run to completion. -When using a faux model, it's a good idea to call +Product.reset_column_information+ to refresh the +ActiveRecord+ cache for the +Product+ model prior to updating data in the database. +When using a faux model, it's a good idea to call ++Product.reset_column_information+ to refresh the +ActiveRecord+ cache for the ++Product+ model prior to updating data in the database. If Alice had done this instead, there would have been no problem: @@ -596,9 +774,11 @@ class AddFlagToProduct < ActiveRecord::Migration end def change - add_column :products, :flag, :int + add_column :products, :flag, :integer Product.reset_column_information - Product.all.each { |f| f.update_attributes!(:flag => false) } + Product.all.each do |product| + product.update_attributes!(:flag => false) + end end end </ruby> @@ -609,32 +789,50 @@ end class AddFuzzToProduct < ActiveRecord::Migration class Product < ActiveRecord::Base end + def change add_column :products, :fuzz, :string Product.reset_column_information - Product.all.each { |f| f.update_attributes! :fuzz => 'fuzzy' } + Product.all.each do |product| + product.update_attributes!(:fuzz => 'fuzzy') + end end end </ruby> - h3. Schema Dumping and You h4. What are Schema Files for? -Migrations, mighty as they may be, are not the authoritative source for your database schema. That role falls to either +db/schema.rb+ or an SQL file which Active Record generates by examining the database. They are not designed to be edited, they just represent the current state of the database. +Migrations, mighty as they may be, are not the authoritative source for your +database schema. That role falls to either +db/schema.rb+ or an SQL file which +Active Record generates by examining the database. They are not designed to be +edited, they just represent the current state of the database. -There is no need (and it is error prone) to deploy a new instance of an app by replaying the entire migration history. It is much simpler and faster to just load into the database a description of the current schema. +There is no need (and it is error prone) to deploy a new instance of an app by +replaying the entire migration history. It is much simpler and faster to just +load into the database a description of the current schema. -For example, this is how the test database is created: the current development database is dumped (either to +db/schema.rb+ or +db/development.sql+) and then loaded into the test database. +For example, this is how the test database is created: the current development +database is dumped (either to +db/schema.rb+ or +db/development.sql+) and then +loaded into the test database. -Schema files are also useful if you want a quick look at what attributes an Active Record object has. This information is not in the model's code and is frequently spread across several migrations, but is summed up in the schema file. The "annotate_models":https://github.com/ctran/annotate_models gem automatically adds and updates comments at the top of each model summarizing the schema if you desire that functionality. +Schema files are also useful if you want a quick look at what attributes an +Active Record object has. This information is not in the model's code and is +frequently spread across several migrations, but the information is nicely +summed up in the schema file. The +"annotate_models":https://github.com/ctran/annotate_models gem automatically +adds and updates comments at the top of each model summarizing the schema if +you desire that functionality. h4. Types of Schema Dumps -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+. +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: +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: <ruby> ActiveRecord::Schema.define(:version => 20080906171750) do @@ -646,28 +844,57 @@ ActiveRecord::Schema.define(:version => 20080906171750) do create_table "products", :force => true do |t| t.string "name" - t.text "description" + t.text "description" t.datetime "created_at" t.datetime "updated_at" - t.string "part_number" + t.string "part_number" end end </ruby> -In many ways this is exactly what it is. This file is created by inspecting the database and expressing its structure using +create_table+, +add_index+, and so on. Because this is database-independent, it could be loaded into any database that Active Record supports. This could be very useful if you were to distribute an application that is able to run against multiple databases. - -There is however a trade-off: +db/schema.rb+ cannot express database specific items such as foreign key constraints, triggers, or stored procedures. While in a migration you can execute custom SQL statements, the schema dumper cannot reconstitute those statements from the database. If you are using features like this, then you should set the schema format to +:sql+. - -Instead of using Active Record's schema dumper, the database's structure will be dumped using a tool specific to the database (via the +db:structure:dump+ Rake task) into +db/structure.sql+. For example, for the PostgreSQL RDBMS, the +pg_dump+ utility is used. For MySQL, this file will contain the output of +SHOW CREATE TABLE+ for the various tables. Loading these schemas is simply a question of executing the SQL statements they contain. By definition, this will create a perfect copy of the database's structure. Using the +:sql+ schema format will, however, prevent loading the schema into a RDBMS other than the one used to create it. +In many ways this is exactly what it is. This file is created by inspecting the +database and expressing its structure using +create_table+, +add_index+, and so +on. Because this is database-independent, it could be loaded into any database +that Active Record supports. This could be very useful if you were to distribute +an application that is able to run against multiple databases. + +There is however a trade-off: +db/schema.rb+ cannot express database specific +items such as foreign key constraints, triggers, or stored procedures. While in +a migration you can execute custom SQL statements, the schema dumper cannot +reconstitute those statements from the database. If you are using features like +this, then you should set the schema format to +:sql+. + +Instead of using Active Record's schema dumper, the database's structure will be +dumped using a tool specific to the database (via the +db:structure:dump+ Rake task) +into +db/structure.sql+. For example, for the PostgreSQL RDBMS, the ++pg_dump+ utility is used. For MySQL, this file will contain the output of +SHOW +CREATE TABLE+ for the various tables. Loading these schemas is simply a question +of executing the SQL statements they contain. By definition, this will create a +perfect copy of the database's structure. Using the +:sql+ schema format will, +however, prevent loading the schema into a RDBMS other than the one used to +create it. h4. Schema Dumps and Source Control -Because schema dumps are the authoritative source for your database schema, it is strongly recommended that you check them into source control. +Because schema dumps are the authoritative source for your database schema, it +is strongly recommended that you check them into source control. 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 :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":https://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+). +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 :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 in the database. + +Although Active Record does not provide any tools for working directly with such +features, the +execute+ method can be used to execute arbitrary SQL. You could +also use some plugin like "foreigner":https://github.com/matthuhiggins/foreigner +which add foreign key support to Active Record (including support for dumping +foreign keys in +db/schema.rb+). diff --git a/railties/guides/source/performance_testing.textile b/railties/guides/source/performance_testing.textile index 2440927542..958b13cd9e 100644 --- a/railties/guides/source/performance_testing.textile +++ b/railties/guides/source/performance_testing.textile @@ -151,7 +151,7 @@ Performance tests can be run in two modes: Benchmarking and Profiling. h5. Benchmarking -Benchmarking makes it easy to quickly gather a few metrics about each test tun. By default, each test case is run *4 times* in benchmarking mode. +Benchmarking makes it easy to quickly gather a few metrics about each test run. By default, each test case is run *4 times* in benchmarking mode. To run performance tests in benchmarking mode: diff --git a/railties/guides/source/rails_on_rack.textile b/railties/guides/source/rails_on_rack.textile index d6cbd84b1f..9526526bc7 100644 --- a/railties/guides/source/rails_on_rack.textile +++ b/railties/guides/source/rails_on_rack.textile @@ -95,6 +95,7 @@ use ActiveSupport::Cache::Strategy::LocalCache use Rack::Runtime use Rails::Rack::Logger use ActionDispatch::ShowExceptions +use ActionDispatch::DebugExceptions use ActionDispatch::RemoteIp use Rack::Sendfile use ActionDispatch::Callbacks diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 29c729592b..0823fb14e3 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -655,7 +655,7 @@ You can use the +:constraints+ option to specify a required format on the implic resources :photos, :constraints => {:id => /[A-Z][A-Z][0-9]+/} </ruby> -This declaration constraints the +:id+ parameter to match the supplied regular expression. So, in this case, the router would no longer match +/photos/1+ to this route. Instead, +/photos/RR27+ would match. +This declaration constrains the +:id+ parameter to match the supplied regular expression. So, in this case, the router would no longer match +/photos/1+ to this route. Instead, +/photos/RR27+ would match. You can specify a single constraint to apply to a number of routes by using the block form: diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 25ff74506a..b20634c5a9 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/hash/reverse_merge' -require 'active_support/file_update_checker' require 'fileutils' require 'rails/plugin' require 'rails/engine' @@ -33,6 +32,25 @@ module Rails # # The Application is also responsible for building the middleware stack. # + # == Booting process + # + # The application is also responsible for setting up and executing the booting + # process. From the moment you require "config/application.rb" in your app, + # the booting process goes like this: + # + # 1) require "config/boot.rb" to setup load paths + # 2) require railties and engines + # 3) Define Rails.application as "class MyApp::Application < Rails::Application" + # 4) Run config.before_configuration callbacks + # 5) Load config/environments/ENV.rb + # 6) Run config.before_initialize callbacks + # 7) Run Railtie#initializer defined by railties, engines and application. + # One by one, each engine sets up its load paths, routes and runs its config/initializers/* files. + # 9) Custom Railtie#initializers added by railties, engines and applications are executed + # 10) Build the middleware stack and run to_prepare callbacks + # 11) Run config.before_eager_load and eager_load if cache classes is true + # 12) Run config.after_initialize callbacks + # class Application < Engine autoload :Bootstrap, 'rails/application/bootstrap' autoload :Configuration, 'rails/application/configuration' @@ -52,12 +70,14 @@ module Rails attr_accessor :assets, :sandbox alias_method :sandbox?, :sandbox + attr_reader :reloaders delegate :default_url_options, :default_url_options=, :to => :routes def initialize super @initialized = false + @reloaders = [] end # This method is called just after an application inherits from Rails::Application, @@ -83,27 +103,51 @@ module Rails require environment if environment end + # Reload application routes regardless if they changed or not. def reload_routes! routes_reloader.reload! end - def routes_reloader + def routes_reloader #:nodoc: @routes_reloader ||= RoutesReloader.new end - def initialize!(group=:default) + # Returns an array of file paths appended with a hash of directories-extensions + # suitable for ActiveSupport::FileUpdateChecker API. + def watchable_args + files = [] + files.concat config.watchable_files + + dirs = {} + dirs.merge! config.watchable_dirs + ActiveSupport::Dependencies.autoload_paths.each do |path| + dirs[path.to_s] = [:rb] + end + + [files, dirs] + end + + # Initialize the application passing the given group. By default, the + # group is :default but sprockets precompilation passes group equals + # to assets if initialize_on_precompile is false to avoid booting the + # whole app. + def initialize!(group=:default) #:nodoc: raise "Application has been already initialized." if @initialized run_initializers(group, self) @initialized = true self end + # Load the application and its railties tasks and invoke the registered hooks. + # Check <tt>Rails::Railtie.rake_tasks</tt> for more info. def load_tasks(app=self) initialize_tasks super self end + # Load the application console and invoke the registered hooks. + # Check <tt>Rails::Railtie.console</tt> for more info. def load_console(app=self) initialize_console super @@ -124,11 +168,14 @@ module Rails "action_dispatch.parameter_filter" => config.filter_parameters, "action_dispatch.secret_token" => config.secret_token, "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions, - "action_dispatch.logger" => Rails.logger + "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local, + "action_dispatch.logger" => Rails.logger, + "action_dispatch.backtrace_cleaner" => Rails.backtrace_cleaner }) end - def ordered_railties + # Returns the ordered railties for this application considering railties_order. + def ordered_railties #:nodoc: @ordered_railties ||= begin order = config.railties_order.map do |railtie| if railtie == :main_app @@ -150,13 +197,13 @@ module Rails end end - def initializers + def initializers #:nodoc: Bootstrap.initializers_for(self) + super + Finisher.initializers_for(self) end - def config + def config #:nodoc: @config ||= Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd)) end @@ -164,7 +211,7 @@ module Rails self end - def helpers_paths + def helpers_paths #:nodoc: config.helpers_paths end @@ -172,6 +219,10 @@ module Rails alias :build_middleware_stack :app + def reload_dependencies? + config.reload_classes_only_on_change != true || reloaders.map(&:updated?).any? + end + def default_middleware_stack ActionDispatch::MiddlewareStack.new.tap do |middleware| if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache @@ -193,12 +244,19 @@ module Rails middleware.use ::Rack::MethodOverride middleware.use ::ActionDispatch::RequestId middleware.use ::Rails::Rack::Logger, config.log_tags # must come after Rack::MethodOverride to properly log overridden methods - middleware.use ::ActionDispatch::ShowExceptions + middleware.use ::ActionDispatch::ShowExceptions, config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path) + middleware.use ::ActionDispatch::DebugExceptions middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies + if config.action_dispatch.x_sendfile_header.present? middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header end - middleware.use ::ActionDispatch::Reloader unless config.cache_classes + + unless config.cache_classes + app = self + middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? } + end + middleware.use ::ActionDispatch::Callbacks middleware.use ::ActionDispatch::Cookies @@ -218,7 +276,7 @@ module Rails end end - def initialize_tasks + def initialize_tasks #:nodoc: self.class.rake_tasks do require "rails/tasks" task :environment do @@ -228,7 +286,7 @@ module Rails end end - def initialize_console + def initialize_console #:nodoc: require "pp" require "rails/console/app" require "rails/console/helpers" diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index c2cb121e42..e189009cc5 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -24,9 +24,18 @@ module Rails initializer :initialize_logger, :group => :all do Rails.logger ||= config.logger || begin path = config.paths["log"].first - logger = ActiveSupport::TaggedLogging.new(ActiveSupport::BufferedLogger.new(path)) + unless File.exist? File.dirname path + FileUtils.mkdir_p File.dirname path + end + + f = File.open path, 'w' + f.binmode + f.sync = !Rails.env.production? # make sure every write flushes + + logger = ActiveSupport::TaggedLogging.new( + ActiveSupport::BufferedLogger.new(f) + ) logger.level = ActiveSupport::BufferedLogger.const_get(config.log_level.to_s.upcase) - logger.auto_flushing = false if Rails.env.production? logger rescue StandardError logger = ActiveSupport::TaggedLogging.new(ActiveSupport::BufferedLogger.new(STDERR)) @@ -37,7 +46,6 @@ module Rails ) logger end - at_exit { Rails.logger.flush if Rails.logger.respond_to?(:flush) } end # Initialize cache early in the stack so railties can make use of it. @@ -51,13 +59,6 @@ module Rails end end - initializer :set_clear_dependencies_hook, :group => :all do - ActionDispatch::Reloader.to_cleanup do - ActiveSupport::DescendantsTracker.clear - ActiveSupport::Dependencies.clear - end - end - # Sets the dependency loading mechanism. # TODO: Remove files from the $" and always use require. initializer :initialize_dependency_mechanism, :group => :all do diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index e95b0f5495..a782441a21 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/string/encoding' require 'active_support/core_ext/kernel/reporting' +require 'active_support/file_update_checker' require 'rails/engine/configuration' module Rails @@ -7,11 +8,11 @@ module Rails class Configuration < ::Rails::Engine::Configuration attr_accessor :allow_concurrency, :asset_host, :asset_path, :assets, :cache_classes, :cache_store, :consider_all_requests_local, - :dependency_loading, :filter_parameters, + :dependency_loading, :exceptions_app, :file_watcher, :filter_parameters, :force_ssl, :helpers_paths, :logger, :log_tags, :preload_frameworks, - :reload_plugins, :secret_token, :serve_static_assets, - :ssl_options, :static_cache_control, :session_options, - :time_zone, :whiny_nils, :railties_order + :railties_order, :relative_url_root, :reload_plugins, :secret_token, + :serve_static_assets, :ssl_options, :static_cache_control, :session_options, + :time_zone, :reload_classes_only_on_change, :whiny_nils attr_writer :log_level attr_reader :encoding @@ -19,23 +20,27 @@ module Rails def initialize(*) super self.encoding = "utf-8" - @allow_concurrency = false - @consider_all_requests_local = false - @filter_parameters = [] - @helpers_paths = [] - @dependency_loading = true - @serve_static_assets = true - @static_cache_control = nil - @force_ssl = false - @ssl_options = {} - @session_store = :cookie_store - @session_options = {} - @time_zone = "UTC" - @log_level = nil - @middleware = app_middleware - @generators = app_generators - @cache_store = [ :file_store, "#{root}/tmp/cache/" ] - @railties_order = [:all] + @allow_concurrency = false + @consider_all_requests_local = false + @filter_parameters = [] + @helpers_paths = [] + @dependency_loading = true + @serve_static_assets = true + @static_cache_control = nil + @force_ssl = false + @ssl_options = {} + @session_store = :cookie_store + @session_options = {} + @time_zone = "UTC" + @log_level = nil + @middleware = app_middleware + @generators = app_generators + @cache_store = [ :file_store, "#{root}/tmp/cache/" ] + @railties_order = [:all] + @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] + @reload_classes_only_on_change = true + @file_watcher = ActiveSupport::FileUpdateChecker + @exceptions_app = nil @assets = ActiveSupport::OrderedOptions.new @assets.enabled = false diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 028c8814c4..b9944bed26 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -2,6 +2,7 @@ module Rails class Application module Finisher include Initializable + $rails_rake_task = nil initializer :add_generator_templates do config.generators.templates.unshift(*paths["lib/templates"].existent) @@ -19,12 +20,6 @@ module Rails end end - initializer :add_to_prepare_blocks do - config.to_prepare_blocks.each do |block| - ActionDispatch::Reloader.to_prepare(&block) - end - end - initializer :add_builtin_route do |app| if Rails.env.development? app.routes.append do @@ -37,14 +32,22 @@ module Rails build_middleware_stack end - initializer :run_prepare_callbacks do - ActionDispatch::Reloader.prepare! - end - initializer :define_main_app_helper do |app| app.routes.define_mounted_helper(:main_app) end + initializer :add_to_prepare_blocks do + config.to_prepare_blocks.each do |block| + ActionDispatch::Reloader.to_prepare(&block) + end + end + + # This needs to happen before eager load so it happens + # in exactly the same point regardless of config.cache_classes + initializer :run_prepare_callbacks do + ActionDispatch::Reloader.prepare! + end + initializer :eager_load! do if config.cache_classes && !$rails_rake_task ActiveSupport.run_load_hooks(:before_eager_load, self) @@ -52,17 +55,37 @@ module Rails end end + # All initialization is done, including eager loading in production initializer :finisher_hook do ActiveSupport.run_load_hooks(:after_initialize, self) end - # Force routes to be loaded just at the end and add it to to_prepare callbacks - # This needs to be after the finisher hook to ensure routes added in the hook - # are still loaded. - initializer :set_routes_reloader do |app| - reloader = lambda { app.routes_reloader.execute_if_updated } - reloader.call - ActionDispatch::Reloader.to_prepare(&reloader) + # Set app reload just after the finisher hook to ensure + # routes added in the hook are still loaded. + initializer :set_routes_reloader_hook do + reloader = routes_reloader + reloader.execute_if_updated + self.reloaders << reloader + ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated } + end + + # Set app reload just after the finisher hook to ensure + # paths added in the hook are still loaded. + initializer :set_clear_dependencies_hook, :group => :all do + callback = lambda do + ActiveSupport::DescendantsTracker.clear + ActiveSupport::Dependencies.clear + end + + if config.reload_classes_only_on_change + reloader = config.file_watcher.new(*watchable_args, &callback) + self.reloaders << reloader + # We need to set a to_prepare callback regardless of the reloader result, i.e. + # models should be reloaded if any of the reloaders (i18n, routes) were updated. + ActionDispatch::Reloader.to_prepare(:prepend => true){ reloader.execute } + else + ActionDispatch::Reloader.to_cleanup(&callback) + end end # Disable dependency loading during request cycle diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb index 1d1f5e1b06..ef7e733ce4 100644 --- a/railties/lib/rails/application/routes_reloader.rb +++ b/railties/lib/rails/application/routes_reloader.rb @@ -1,10 +1,13 @@ +require "active_support/core_ext/module/delegation" + module Rails class Application - class RoutesReloader < ::ActiveSupport::FileUpdateChecker - attr_reader :route_sets + class RoutesReloader + attr_reader :route_sets, :paths + delegate :execute_if_updated, :execute, :updated?, :to => :updater def initialize - super([]) { reload! } + @paths = [] @route_sets = [] end @@ -16,7 +19,15 @@ module Rails revert end - protected + private + + def updater + @updater ||= begin + updater = ActiveSupport::FileUpdateChecker.new(paths) { reload! } + updater.execute + updater + end + end def clear! route_sets.each do |routes| diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 4b0acc9d88..d425c9db6c 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -41,7 +41,7 @@ module Rails abort opt.to_s unless (0..1).include?(ARGV.size) end - unless config = YAML::load(ERB.new(IO.read("#{@app.root}/config/database.yml")).result)[Rails.env] + unless config = @app.config.database_configuration[Rails.env] abort "No database is configured for the environment '#{Rails.env}'" end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 5c1af99fe2..20321a502d 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -228,7 +228,7 @@ module Rails # resources :articles # end # - # The routes above will automatically point to <tt>MyEngine::ApplicationContoller</tt>. Furthermore, you don't + # The routes above will automatically point to <tt>MyEngine::ApplicationController</tt>. Furthermore, you don't # need to use longer url helpers like <tt>my_engine_articles_path</tt>. Instead, you should simply use # <tt>articles_path</tt> as you would do with your application. # @@ -517,7 +517,7 @@ module Rails # Blog::Engine.load_seed def load_seed seed_file = paths["db/seeds"].existent.first - load(seed_file) if seed_file && File.exist?(seed_file) + load(seed_file) if seed_file end # Add configured load paths to ruby load paths and remove duplicates. diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index f424492bb4..d6015e9c01 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -30,7 +30,7 @@ module Rails # # config.generators.colorize_logging = false # - def generators #:nodoc + def generators #:nodoc: @generators ||= Rails::Configuration::Generators.new yield(@generators) if block_given? @generators diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 3fbde0d989..0bd39cb2cd 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -193,7 +193,8 @@ module Rails end def assets_gemfile_entry - <<-GEMFILE.strip_heredoc + return if options[:skip_sprockets] + <<-GEMFILE.strip_heredoc.gsub(/^[ \t]*$/, '') # Gems used only for assets and not required # in production environments by default. group :assets do diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 3e32f758a4..2a6bd57df4 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -38,7 +38,7 @@ module Rails end def readme - copy_file "README" + copy_file "README", "README.rdoc" end def gemfile diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index d3b8f4d595..c260cc999b 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -1,4 +1,4 @@ -source 'http://rubygems.org' +source 'https://rubygems.org' <%= rails_gemfile_entry -%> 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 40fd843b1b..c6dfa1f2dd 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -54,6 +54,12 @@ module <%= app_const_base %> # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql + # Enforce whitelist mode for mass assignment. + # This will create an empty whitelist of attributes available for mass-assignment for all models + # in your app. As such, your models will need to explicitly whitelist or blacklist accessible + # parameters by using an attr_accessible or attr_protected declaration. + # config.active_record.whitelist_attributes = true + <% unless options.skip_sprockets? -%> # Enable the asset pipeline config.assets.enabled = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index 47078e3af9..9e53b6a70d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -25,11 +25,17 @@ <%- unless options.skip_active_record? -%> # Raise exception on mass assignment protection for ActiveRecord models config.active_record.mass_assignment_sanitizer = :strict + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + config.active_record.auto_explain_threshold_in_seconds = 0.5 <%- end -%> + <%- unless options.skip_sprockets? -%> # Do not compress assets config.assets.compress = false # Expands the lines which load the assets config.assets.debug = true + <%- end -%> end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 50f2df3d35..0f571f7c1a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -11,6 +11,7 @@ # Disable Rails's static asset server (Apache or nginx will already do this) config.serve_static_assets = false + <%- unless options.skip_sprockets? -%> # Compress JavaScripts and CSS config.assets.compress = true @@ -22,6 +23,7 @@ # Defaults to Rails.root.join("public/assets") # config.assets.manifest = YOUR_PATH + <%- end -%> # Specifies the header that your server uses for sending files # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache @@ -45,8 +47,10 @@ # Enable serving of images, stylesheets, and JavaScripts from an asset server # config.action_controller.asset_host = "http://assets.example.com" + <%- unless options.skip_sprockets? -%> # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) # config.assets.precompile += %w( search.js ) + <%- end -%> # Disable delivery errors, bad email addresses will be ignored # config.action_mailer.raise_delivery_errors = false @@ -60,4 +64,10 @@ # Send deprecation notices to registered listeners config.active_support.deprecation = :notify + + <%- unless options.skip_active_record? -%> + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + # config.active_record.auto_explain_threshold_in_seconds = 0.5 + <%- end -%> end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/gitignore b/railties/lib/rails/generators/rails/plugin_new/templates/gitignore index 92bd3c614b..7833c586f3 100644 --- a/railties/lib/rails/generators/rails/plugin_new/templates/gitignore +++ b/railties/lib/rails/generators/rails/plugin_new/templates/gitignore @@ -3,4 +3,5 @@ log/*.log pkg/ <%= dummy_path %>/db/*.sqlite3 <%= dummy_path %>/log/*.log -<%= dummy_path %>/tmp/
\ No newline at end of file +<%= dummy_path %>/tmp/ +<%= dummy_path %>/.sass-cache diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb index f888684117..cf9e4ad500 100644 --- a/railties/lib/rails/railtie/configuration.rb +++ b/railties/lib/rails/railtie/configuration.rb @@ -7,6 +7,18 @@ module Rails @@options ||= {} end + # Add files that should be watched for change. + def watchable_files + @@watchable_files ||= [] + end + + # Add directories that should be watched for change. + # The key of the hashes should be directories and the values should + # be an array of extensions to match in each directory. + def watchable_dirs + @@watchable_dirs ||= {} + end + # This allows you to modify the application's middlewares from Engines. # # All operations you run on the app_middleware will be replayed on the diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 1eed763aa3..2286e0477a 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -30,7 +30,7 @@ class SourceAnnotationExtractor # Prints all annotations with tag +tag+ under the root directories +app+, +config+, +lib+, # +script+, and +test+ (recursively). Only filenames with extension - # +.builder+, +.rb+, +.rxml+, +.rhtml+, or +.erb+ are taken into account. The +options+ + # +.builder+, +.rb+, and +.erb+ are taken into account. The +options+ # hash is passed to each annotation's +to_s+. # # This class method is the single entry point for the rake tasks. @@ -46,16 +46,14 @@ class SourceAnnotationExtractor end # Returns a hash that maps filenames under +dirs+ (recursively) to arrays - # with their annotations. Only files with annotations are included, and only - # those with extension +.builder+, +.rb+, +.rxml+, +.rhtml+, and +.erb+ - # are taken into account. + # with their annotations. def find(dirs=%w(app config lib script test)) dirs.inject({}) { |h, dir| h.update(find_in(dir)) } end # Returns a hash that maps filenames under +dir+ (recursively) to arrays # with their annotations. Only files with annotations are included, and only - # those with extension +.builder+, +.rb+, +.rxml+, +.rhtml+, and +.erb+ + # those with extension +.builder+, +.rb+, +.erb+, +.haml+ and +.slim+ # are taken into account. def find_in(dir) results = {} @@ -65,10 +63,14 @@ class SourceAnnotationExtractor if File.directory?(item) results.update(find_in(item)) - elsif item =~ /\.(builder|(r(?:b|xml|js)))$/ + elsif item =~ /\.(builder|rb)$/ results.update(extract_annotations_from(item, /#\s*(#{tag}):?\s*(.*)$/)) - elsif item =~ /\.(rhtml|erb)$/ + elsif item =~ /\.erb$/ results.update(extract_annotations_from(item, /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/)) + elsif item =~ /\.haml$/ + results.update(extract_annotations_from(item, /-\s*#\s*(#{tag}):?\s*(.*)$/)) + elsif item =~ /\.slim$/ + results.update(extract_annotations_from(item, /\/\s*\s*(#{tag}):?\s*(.*)$/)) end end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index d4ffbe3d66..cc5695091b 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -72,7 +72,7 @@ module ApplicationTests end end - test "precompile application.js and application.css and all other files not ending with .js or .css by default" do + test "precompile application.js and application.css and all other non JS/CSS files" do app_file "app/assets/javascripts/application.js", "alert();" app_file "app/assets/stylesheets/application.css", "body{}" @@ -82,8 +82,11 @@ module ApplicationTests app_file "app/assets/javascripts/something.min.js", "alert();" app_file "app/assets/stylesheets/something.min.css", "body{}" + app_file "app/assets/javascripts/something.else.js.erb", "alert();" + app_file "app/assets/stylesheets/something.else.css.erb", "body{}" + images_should_compile = ["a.png", "happyface.png", "happy_face.png", "happy.face.png", - "happy-face.png", "happy.happy_face.png", "happy_happy.face.png", + "happy-face.png", "happy.happy_face.png", "happy_happy.face.png", "happy.happy.face.png", "happy", "happy.face", "-happyface", "-happy.png", "-happy.face.png", "_happyface", "_happy.face.png", "_happy.png"] @@ -106,6 +109,9 @@ module ApplicationTests assert !File.exists?("#{app_path}/public/assets/something.min.js") assert !File.exists?("#{app_path}/public/assets/something.min.css") + + assert !File.exists?("#{app_path}/public/assets/something.else.js") + assert !File.exists?("#{app_path}/public/assets/something.else.css") end test "asset pipeline should use a Sprockets::Index when config.assets.digest is true" do @@ -213,7 +219,9 @@ module ApplicationTests app_file "app/assets/javascripts/app.js", "alert();" require "#{app_path}/config/environment" - class ::PostsController < ActionController::Base ; end + class ::PostsController < ActionController::Base + def show_detailed_exceptions?() true end + end get '/posts' assert_match(/AssetNotPrecompiledError/, last_response.body) @@ -451,6 +459,37 @@ module ApplicationTests assert_equal 0, files.length, "Expected application.js asset to be removed, but still exists" end + test "asset urls should use the request's protocol by default" do + app_with_assets_in_view + add_to_config "config.asset_host = 'example.com'" + require "#{app_path}/config/environment" + class ::PostsController < ActionController::Base; end + + get '/posts', {}, {'HTTPS'=>'off'} + assert_match('src="http://example.com/assets/application.js', last_response.body) + get '/posts', {}, {'HTTPS'=>'on'} + assert_match('src="https://example.com/assets/application.js', last_response.body) + end + + test "asset urls should be protocol-relative if no request is in scope" do + app_file "app/assets/javascripts/image_loader.js.erb", 'var src="<%= image_path("rails.png") %>";' + add_to_config "config.assets.precompile = %w{image_loader.js}" + add_to_config "config.asset_host = 'example.com'" + precompile! + + assert_match 'src="//example.com/assets/rails.png"', File.read("#{app_path}/public/assets/image_loader.js") + end + + test "asset paths should use RAILS_RELATIVE_URL_ROOT by default" do + ENV["RAILS_RELATIVE_URL_ROOT"] = "/sub/uri" + + app_file "app/assets/javascripts/app.js.erb", 'var src="<%= image_path("rails.png") %>";' + add_to_config "config.assets.precompile = %w{app.js}" + precompile! + + assert_match 'src="/sub/uri/assets/rails.png"', File.read("#{app_path}/public/assets/app.js") + end + private def app_with_assets_in_view diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index f37a024a0b..28ffff58ca 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -521,10 +521,11 @@ module ApplicationTests make_basic_app assert_respond_to app, :env_config - assert_equal app.env_config['action_dispatch.parameter_filter'], app.config.filter_parameters - assert_equal app.env_config['action_dispatch.secret_token'], app.config.secret_token - assert_equal app.env_config['action_dispatch.show_exceptions'], app.config.action_dispatch.show_exceptions - assert_equal app.env_config['action_dispatch.logger'], Rails.logger + assert_equal app.env_config['action_dispatch.parameter_filter'], app.config.filter_parameters + assert_equal app.env_config['action_dispatch.secret_token'], app.config.secret_token + assert_equal app.env_config['action_dispatch.show_exceptions'], app.config.action_dispatch.show_exceptions + assert_equal app.env_config['action_dispatch.logger'], Rails.logger + assert_equal app.env_config['action_dispatch.backtrace_cleaner'], Rails.backtrace_cleaner end end end diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 2073c780bf..fa2652a6d3 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -61,7 +61,6 @@ class ConsoleTest < Test::Unit::TestCase load_environment assert User.new.respond_to?(:name) - assert !User.new.respond_to?(:age) app_file "app/models/user.rb", <<-MODEL class User diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 446c85d65a..cf6c4d8fc2 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -136,6 +136,13 @@ module ApplicationTests assert_equal 2, ActionDispatch::Http::URL.tld_length end + test "assignment config.encoding to default_charset" do + charset = "ruby".respond_to?(:force_encoding) ? 'Shift_JIS' : 'UTF8' + add_to_config "config.encoding = '#{charset}'" + require "#{app_path}/config/environment" + assert_equal charset, ActionDispatch::Response.default_charset + end + # AS test "if there's no config.active_support.bare, all of ActiveSupport is required" do use_frameworks [] diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index 8c2c079fb8..305ae7eb0a 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -120,6 +120,9 @@ en: get "/i18n" assert_equal "1", last_response.body + # Wait a full second so we have time for changes to propagate + sleep(1) + app_file "config/locales/en.yml", <<-YAML en: foo: "2" diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 47c6fd5c6e..c4c93cce22 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -16,7 +16,7 @@ class LoadingTest < Test::Unit::TestCase @app ||= Rails.application end - def test_constants_in_app_are_autoloaded + test "constants in app are autoloaded" do app_file "app/models/post.rb", <<-MODEL class Post < ActiveRecord::Base validates_acceptance_of :title, :accept => "omg" @@ -33,7 +33,7 @@ class LoadingTest < Test::Unit::TestCase assert_equal 'omg', p.title end - def test_models_without_table_do_not_panic_on_scope_definitions_when_loaded + test "models without table do not panic on scope definitions when loaded" do app_file "app/models/user.rb", <<-MODEL class User < ActiveRecord::Base default_scope where(:published => true) @@ -63,9 +63,10 @@ class LoadingTest < Test::Unit::TestCase assert ::AppTemplate::Application.config.loaded end - def test_descendants_are_cleaned_on_each_request_without_cache_classes + test "descendants are cleaned on each request without cache classes" do add_to_config <<-RUBY config.cache_classes = false + config.reload_classes_only_on_change = false RUBY app_file "app/models/post.rb", <<-MODEL @@ -98,6 +99,162 @@ class LoadingTest < Test::Unit::TestCase assert_raise(RuntimeError) { ::AppTemplate::Application.initialize! } end + test "reload constants on development" do + add_to_config <<-RUBY + config.cache_classes = false + RUBY + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + match '/c', :to => lambda { |env| [200, {"Content-Type" => "text/plain"}, [User.counter.to_s]] } + end + RUBY + + app_file "app/models/user.rb", <<-MODEL + class User + def self.counter; 1; end + end + MODEL + + require 'rack/test' + extend Rack::Test::Methods + + require "#{rails_root}/config/environment" + + get "/c" + assert_equal "1", last_response.body + + app_file "app/models/user.rb", <<-MODEL + class User + def self.counter; 2; end + end + MODEL + + get "/c" + assert_equal "2", last_response.body + end + + test "does not reload constants on development if custom file watcher always returns false" do + add_to_config <<-RUBY + config.cache_classes = false + config.file_watcher = Class.new do + def initialize(*); end + def updated?; false; end + end + RUBY + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + match '/c', :to => lambda { |env| [200, {"Content-Type" => "text/plain"}, [User.counter.to_s]] } + end + RUBY + + app_file "app/models/user.rb", <<-MODEL + class User + def self.counter; 1; end + end + MODEL + + require 'rack/test' + extend Rack::Test::Methods + + require "#{rails_root}/config/environment" + + get "/c" + assert_equal "1", last_response.body + + app_file "app/models/user.rb", <<-MODEL + class User + def self.counter; 2; end + end + MODEL + + get "/c" + assert_equal "1", last_response.body + end + + test "added files (like db/schema.rb) also trigger reloading" do + add_to_config <<-RUBY + config.cache_classes = false + RUBY + + app_file 'config/routes.rb', <<-RUBY + $counter = 0 + AppTemplate::Application.routes.draw do + match '/c', :to => lambda { |env| User; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } + end + RUBY + + app_file "app/models/user.rb", <<-MODEL + class User + $counter += 1 + end + MODEL + + require 'rack/test' + extend Rack::Test::Methods + + require "#{rails_root}/config/environment" + + get "/c" + assert_equal "1", last_response.body + + app_file "db/schema.rb", "" + + get "/c" + assert_equal "2", last_response.body + end + + test "columns migrations also trigger reloading" do + add_to_config <<-RUBY + config.cache_classes = false + RUBY + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + match '/title', :to => lambda { |env| [200, {"Content-Type" => "text/plain"}, [Post.new.title]] } + match '/body', :to => lambda { |env| [200, {"Content-Type" => "text/plain"}, [Post.new.body]] } + end + RUBY + + app_file "app/models/post.rb", <<-MODEL + class Post < ActiveRecord::Base + end + MODEL + + require 'rack/test' + extend Rack::Test::Methods + + app_file "db/migrate/1_create_posts.rb", <<-MIGRATION + class CreatePosts < ActiveRecord::Migration + def change + create_table :posts do |t| + t.string :title, :default => "TITLE" + end + end + end + MIGRATION + + Dir.chdir(app_path) { `rake db:migrate`} + require "#{rails_root}/config/environment" + + get "/title" + assert_equal "TITLE", last_response.body + + app_file "db/migrate/2_add_body_to_posts.rb", <<-MIGRATION + class AddBodyToPosts < ActiveRecord::Migration + def change + add_column :posts, :body, :text, :default => "BODY" + end + end + MIGRATION + + Dir.chdir(app_path) { `rake db:migrate` } + + get "/body" + assert_equal "BODY", last_response.body + end + protected def setup_ar! diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb index 050a2161ae..790c5b2d53 100644 --- a/railties/test/application/middleware/cache_test.rb +++ b/railties/test/application/middleware/cache_test.rb @@ -54,9 +54,9 @@ module ApplicationTests def test_cache_keeps_if_modified_since simple_controller expected = "Wed, 30 May 1984 19:43:31 GMT" - + get "/expires/keeps_if_modified_since", {}, "HTTP_IF_MODIFIED_SINCE" => expected - + assert_equal 200, last_response.status assert_equal expected, last_response.body, "cache should have kept If-Modified-Since" end diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb new file mode 100644 index 0000000000..a9cde42be8 --- /dev/null +++ b/railties/test/application/middleware/exceptions_test.rb @@ -0,0 +1,115 @@ +# encoding: utf-8 +require 'isolation/abstract_unit' +require 'rack/test' + +module ApplicationTests + class MiddlewareExceptionsTest < Test::Unit::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + def setup + build_app + boot_rails + end + + def teardown + teardown_app + end + + test "show exceptions middleware filter backtrace before logging" do + my_middleware = Struct.new(:app) do + def call(env) + raise "Failure" + end + end + + app.config.middleware.use my_middleware + + stringio = StringIO.new + Rails.logger = Logger.new(stringio) + + get "/" + assert_no_match(/action_dispatch/, stringio.string) + end + + test "renders active record exceptions as 404" do + my_middleware = Struct.new(:app) do + def call(env) + raise ActiveRecord::RecordNotFound + end + end + + app.config.middleware.use my_middleware + + get "/" + assert_equal 404, last_response.status + end + + test "uses custom exceptions app" do + add_to_config <<-RUBY + config.exceptions_app = lambda do |env| + [404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]] + end + RUBY + + app.config.action_dispatch.show_exceptions = true + + get "/foo" + assert_equal 404, last_response.status + assert_equal "YOU FAILED BRO", last_response.body + end + + test "unspecified route when action_dispatch.show_exceptions is not set raises an exception" do + app.config.action_dispatch.show_exceptions = false + + assert_raise(ActionController::RoutingError) do + get '/foo' + end + end + + test "unspecified route when action_dispatch.show_exceptions is set shows 404" do + app.config.action_dispatch.show_exceptions = true + + assert_nothing_raised(ActionController::RoutingError) do + get '/foo' + assert_match "The page you were looking for doesn't exist.", last_response.body + end + end + + test "unspecified route when action_dispatch.show_exceptions and consider_all_requests_local are set shows diagnostics" do + app.config.action_dispatch.show_exceptions = true + app.config.consider_all_requests_local = true + + assert_nothing_raised(ActionController::RoutingError) do + get '/foo' + assert_match "No route matches", last_response.body + end + end + + test "displays diagnostics message when exception raised in template that contains UTF-8" do + app.config.action_dispatch.show_exceptions = true + app.config.consider_all_requests_local = true + + controller :foo, <<-RUBY + class FooController < ActionController::Base + def index + end + end + RUBY + + app_file 'app/views/foo/index.html.erb', <<-ERB + <% raise 'boooom' %> + ✓ + ERB + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + match ':controller(/:action)' + end + RUBY + + post '/foo', :utf8 => '✓' + assert_match(/boooom/, last_response.body) + end + end +end diff --git a/railties/test/application/middleware/show_exceptions_test.rb b/railties/test/application/middleware/show_exceptions_test.rb deleted file mode 100644 index e3f27f63c3..0000000000 --- a/railties/test/application/middleware/show_exceptions_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'isolation/abstract_unit' - -module ApplicationTests - class ShowExceptionsTest < Test::Unit::TestCase - include ActiveSupport::Testing::Isolation - - def setup - build_app - boot_rails - FileUtils.rm_rf "#{app_path}/config/environments" - end - - def teardown - teardown_app - end - - def app - @app ||= Rails.application - end - - test "unspecified route when set action_dispatch.show_exceptions to false" do - make_basic_app do |app| - app.config.action_dispatch.show_exceptions = false - end - - assert_raise(ActionController::RoutingError) do - get '/foo' - end - end - - test "unspecified route when set action_dispatch.show_exceptions to true" do - make_basic_app do |app| - app.config.action_dispatch.show_exceptions = true - end - - assert_nothing_raised(ActionController::RoutingError) do - get '/foo' - end - end - end -end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 4703a59326..578370cfca 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -33,6 +33,7 @@ module ApplicationTests "ActionDispatch::RequestId", "Rails::Rack::Logger", # must come after Rack::MethodOverride to properly log overridden methods "ActionDispatch::ShowExceptions", + "ActionDispatch::DebugExceptions", "ActionDispatch::RemoteIp", "Rack::Sendfile", "ActionDispatch::Reloader", @@ -104,10 +105,11 @@ module ApplicationTests assert !middleware.include?("ActionDispatch::Static") end - test "includes show exceptions even action_dispatch.show_exceptions is disabled" do + test "includes exceptions middlewares even if action_dispatch.show_exceptions is disabled" do add_to_config "config.action_dispatch.show_exceptions = false" boot! assert middleware.include?("ActionDispatch::ShowExceptions") + assert middleware.include?("ActionDispatch::DebugExceptions") end test "removes ActionDispatch::Reloader if cache_classes is true" do @@ -191,26 +193,6 @@ module ApplicationTests assert_equal nil, last_response.headers["Etag"] end - # Show exceptions middleware - test "show exceptions middleware filter backtrace before logging" do - my_middleware = Struct.new(:app) do - def call(env) - raise "Failure" - end - end - - make_basic_app do |app| - app.config.middleware.use my_middleware - end - - stringio = StringIO.new - Rails.logger = Logger.new(stringio) - - env = Rack::MockRequest.env_for("/") - Rails.application.call(env) - assert_no_match(/action_dispatch/, stringio.string) - end - private def boot! diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb new file mode 100644 index 0000000000..7982c42d8f --- /dev/null +++ b/railties/test/application/rake/migrations_test.rb @@ -0,0 +1,109 @@ +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeMigrationsTest < Test::Unit::TestCase + def setup + build_app + boot_rails + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def teardown + teardown_app + end + + test 'running migrations with given scope' do + Dir.chdir(app_path) do + `rails generate model user username:string password:string` + end + app_file "db/migrate/01_a_migration.bukkits.rb", <<-MIGRATION + class AMigration < ActiveRecord::Migration + end + MIGRATION + + output = Dir.chdir(app_path) { `rake db:migrate SCOPE=bukkits` } + assert_no_match(/create_table\(:users\)/, output) + assert_no_match(/CreateUsers/, output) + assert_no_match(/add_column\(:users, :email, :string\)/, output) + + assert_match(/AMigration: migrated/, output) + + output = Dir.chdir(app_path) { `rake db:migrate SCOPE=bukkits VERSION=0` } + assert_no_match(/drop_table\(:users\)/, output) + assert_no_match(/CreateUsers/, output) + assert_no_match(/remove_column\(:users, :email\)/, output) + + assert_match(/AMigration: reverted/, output) + end + + test 'model and migration generator with change syntax' do + Dir.chdir(app_path) do + `rails generate model user username:string password:string` + `rails generate migration add_email_to_users email:string` + end + + output = Dir.chdir(app_path){ `rake db:migrate` } + assert_match(/create_table\(:users\)/, output) + assert_match(/CreateUsers: migrated/, output) + assert_match(/add_column\(:users, :email, :string\)/, output) + assert_match(/AddEmailToUsers: migrated/, output) + + output = Dir.chdir(app_path){ `rake db:rollback STEP=2` } + assert_match(/drop_table\("users"\)/, output) + assert_match(/CreateUsers: reverted/, output) + assert_match(/remove_column\("users", :email\)/, output) + assert_match(/AddEmailToUsers: reverted/, output) + end + + test 'migration status when schema migrations table is not present' do + output = Dir.chdir(app_path){ `rake db:migrate:status` } + assert_equal "Schema migrations table does not exist yet.\n", output + end + + test 'test migration status' do + Dir.chdir(app_path) do + `rails generate model user username:string password:string` + `rails generate migration add_email_to_users email:string` + end + + Dir.chdir(app_path) { `rake db:migrate`} + output = Dir.chdir(app_path) { `rake db:migrate:status` } + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + + Dir.chdir(app_path) { `rake db:rollback STEP=1` } + output = Dir.chdir(app_path) { `rake db:migrate:status` } + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/down\s+\d{14}\s+Add email to users/, output) + end + + test 'test migration status after rollback and redo' do + Dir.chdir(app_path) do + `rails generate model user username:string password:string` + `rails generate migration add_email_to_users email:string` + end + + Dir.chdir(app_path) { `rake db:migrate` } + output = Dir.chdir(app_path) { `rake db:migrate:status` } + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + + Dir.chdir(app_path) { `rake db:rollback STEP=2` } + output = Dir.chdir(app_path) { `rake db:migrate:status` } + + assert_match(/down\s+\d{14}\s+Create users/, output) + assert_match(/down\s+\d{14}\s+Add email to users/, output) + + Dir.chdir(app_path) { `rake db:migrate:redo` } + output = Dir.chdir(app_path) { `rake db:migrate:status` } + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + end + end + end +end diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb new file mode 100644 index 0000000000..659cbfec0f --- /dev/null +++ b/railties/test/application/rake/notes_test.rb @@ -0,0 +1,45 @@ +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeNotesTest < Test::Unit::TestCase + def setup + build_app + require "rails/all" + end + + def teardown + teardown_app + end + + test 'notes' do + + app_file "app/views/home/index.html.erb", "<% # TODO: note in erb %>" + app_file "app/views/home/index.html.haml", "-# TODO: note in haml" + app_file "app/views/home/index.html.slim", "/ TODO: note in slim" + + boot_rails + require 'rake' + require 'rdoc/task' + require 'rake/testtask' + + Rails.application.load_tasks + + Dir.chdir(app_path) do + output = `bundle exec rake notes` + + assert_match /note in erb/, output + assert_match /note in haml/, output + assert_match /note in slim/, output + end + + end + + private + def boot_rails + super + require "#{app_path}/config/environment" + end + end + end +end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index c76bc3d526..4e406f23d2 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -108,74 +108,6 @@ module ApplicationTests assert_match "Sample log message", output end - def test_model_and_migration_generator_with_change_syntax - Dir.chdir(app_path) do - `rails generate model user username:string password:string` - `rails generate migration add_email_to_users email:string` - end - - output = Dir.chdir(app_path){ `rake db:migrate` } - assert_match(/create_table\(:users\)/, output) - assert_match(/CreateUsers: migrated/, output) - assert_match(/add_column\(:users, :email, :string\)/, output) - assert_match(/AddEmailToUsers: migrated/, output) - - output = Dir.chdir(app_path){ `rake db:rollback STEP=2` } - assert_match(/drop_table\("users"\)/, output) - assert_match(/CreateUsers: reverted/, output) - assert_match(/remove_column\("users", :email\)/, output) - assert_match(/AddEmailToUsers: reverted/, output) - end - - def test_migration_status_when_schema_migrations_table_is_not_present - output = Dir.chdir(app_path){ `rake db:migrate:status` } - assert_equal "Schema migrations table does not exist yet.\n", output - end - - def test_migration_status - Dir.chdir(app_path) do - `rails generate model user username:string password:string` - `rails generate migration add_email_to_users email:string` - end - - Dir.chdir(app_path) { `rake db:migrate`} - output = Dir.chdir(app_path) { `rake db:migrate:status` } - - assert_match(/up\s+\d{14}\s+Create users/, output) - assert_match(/up\s+\d{14}\s+Add email to users/, output) - - Dir.chdir(app_path) { `rake db:rollback STEP=1` } - output = Dir.chdir(app_path) { `rake db:migrate:status` } - - assert_match(/up\s+\d{14}\s+Create users/, output) - assert_match(/down\s+\d{14}\s+Add email to users/, output) - end - - def test_migration_status_after_rollback_and_redo - Dir.chdir(app_path) do - `rails generate model user username:string password:string` - `rails generate migration add_email_to_users email:string` - end - - Dir.chdir(app_path) { `rake db:migrate`} - output = Dir.chdir(app_path) { `rake db:migrate:status` } - - assert_match(/up\s+\d{14}\s+Create users/, output) - assert_match(/up\s+\d{14}\s+Add email to users/, output) - - Dir.chdir(app_path) { `rake db:rollback STEP=2` } - output = Dir.chdir(app_path) { `rake db:migrate:status` } - - assert_match(/down\s+\d{14}\s+Create users/, output) - assert_match(/down\s+\d{14}\s+Add email to users/, output) - - Dir.chdir(app_path) { `rake db:migrate:redo` } - output = Dir.chdir(app_path) { `rake db:migrate:status` } - - assert_match(/up\s+\d{14}\s+Create users/, output) - assert_match(/up\s+\d{14}\s+Add email to users/, output) - end - def test_loading_specific_fixtures Dir.chdir(app_path) do `rails generate model user username:string password:string` diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb index 27a7959e84..a06facc04b 100644 --- a/railties/test/application/test_test.rb +++ b/railties/test/application/test_test.rb @@ -24,7 +24,7 @@ module ApplicationTests end RUBY - run_test 'unit/foo_test.rb' + run_test_file 'unit/foo_test.rb' end # Run just in Ruby < 1.9 @@ -40,7 +40,7 @@ module ApplicationTests end RUBY - run_test 'unit/backtrace_test.rb' + run_test_file 'unit/backtrace_test.rb' end end @@ -66,7 +66,7 @@ module ApplicationTests end RUBY - run_test 'integration/posts_test.rb' + run_test_file 'integration/posts_test.rb' end test "performance test" do @@ -91,11 +91,11 @@ module ApplicationTests end RUBY - run_test 'performance/posts_test.rb' + run_test_file 'performance/posts_test.rb' end private - def run_test(name) + def run_test_file(name) result = ruby '-Itest', "#{app_path}/test/#{name}" assert_equal 0, $?.to_i, result end diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index c1fd6a38f1..ee288871de 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -114,7 +114,7 @@ class ActionsTest < Rails::Generators::TestCase action :gem_group, :test do gem 'fakeweb' end - + assert_file 'Gemfile', /\ngroup :development, :test do\n gem "rspec-rails"\nend\n\ngroup :test do\n gem "fakeweb"\nend/ end @@ -233,14 +233,14 @@ class ActionsTest < Rails::Generators::TestCase def test_readme run_generator Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root) - assert_match(/Welcome to Rails/, action(:readme, "README")) + assert_match(/Welcome to Rails/, action(:readme, "README.rdoc")) end def test_readme_with_quiet generator(default_arguments, :quiet => true) run_generator Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root) - assert_no_match(/Welcome to Rails/, action(:readme, "README")) + assert_no_match(/Welcome to Rails/, action(:readme, "README.rdoc")) end def test_log diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 235c08e38e..30fbe74e83 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -124,6 +124,16 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "hats/config/environment.rb", /Hats::Application\.initialize!/ end + def test_gemfile_has_no_whitespace_errors + run_generator + absolute = File.expand_path("Gemfile", destination_root) + File.open(absolute, 'r') do |f| + f.each_line do |line| + assert_no_match /^[ \t]+$/, line + end + end + end + def test_config_database_is_added_by_default run_generator assert_file "config/database.yml", /sqlite3/ @@ -206,13 +216,29 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content) assert_no_match(/config\.assets\.enabled = true/, content) end + assert_file "Gemfile" do |content| + assert_no_match(/sass-rails/, content) + assert_no_match(/coffee-rails/, content) + assert_no_match(/uglifier/, content) + end + assert_file "config/environments/development.rb" do |content| + assert_no_match(/config\.assets\.debug = true/, content) + end + assert_file "config/environments/production.rb" do |content| + assert_no_match(/config\.assets\.digest = true/, content) + assert_no_match(/config\.assets\.compress = true/, content) + end assert_file "test/performance/browsing_test.rb" end def test_inclusion_of_therubyrhino_under_jruby + run_generator([destination_root]) if defined?(JRUBY_VERSION) - run_generator([destination_root]) assert_file "Gemfile", /gem\s+["']therubyrhino["']$/ + else + assert_file "Gemfile" do |content| + assert_no_match(/gem\s+["']therubyrhino["']$/, content) + end end end @@ -329,13 +355,22 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generated_environments_file_for_sanitizer run_generator [destination_root, "--skip-active-record"] - ["config/environments/development.rb", "config/environments/test.rb"].each do |env_file| - assert_file env_file do |file| + %w(development test).each do |env| + assert_file "config/environments/#{env}.rb" do |file| assert_no_match(/config.active_record.mass_assignment_sanitizer = :strict/, file) end end end + def test_generated_environments_file_for_auto_explain + run_generator [destination_root, "--skip-active-record"] + %w(development production).each do |env| + assert_file "config/environments/#{env}.rb" do |file| + assert_no_match %r(auto_explain_threshold_in_seconds), file + end + end + end + protected def action(*args, &block) diff --git a/railties/test/railties/shared_tests.rb b/railties/test/railties/shared_tests.rb index 7653e52d26..a15dae2a0a 100644 --- a/railties/test/railties/shared_tests.rb +++ b/railties/test/railties/shared_tests.rb @@ -56,6 +56,8 @@ module RailtiesTest app_file "db/migrate/1_create_sessions.rb", <<-RUBY class CreateSessions < ActiveRecord::Migration + def up + end end RUBY @@ -76,19 +78,19 @@ module RailtiesTest Dir.chdir(app_path) do output = `bundle exec rake bukkits:install:migrations` - assert File.exists?("#{app_path}/db/migrate/2_create_users.rb") - assert File.exists?("#{app_path}/db/migrate/3_add_last_name_to_users.rb") - assert_match(/Copied migration 2_create_users.rb from bukkits/, output) - assert_match(/Copied migration 3_add_last_name_to_users.rb from bukkits/, output) + assert File.exists?("#{app_path}/db/migrate/2_create_users.bukkits.rb") + assert File.exists?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb") + assert_match(/Copied migration 2_create_users.bukkits.rb from bukkits/, output) + assert_match(/Copied migration 3_add_last_name_to_users.bukkits.rb from bukkits/, output) assert_match(/NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output) assert_equal 3, Dir["#{app_path}/db/migrate/*.rb"].length output = `bundle exec rake railties:install:migrations`.split("\n") - assert File.exists?("#{app_path}/db/migrate/4_create_yaffles.rb") + assert File.exists?("#{app_path}/db/migrate/4_create_yaffles.acts_as_yaffle.rb") assert_no_match(/2_create_users/, output.join("\n")) - yaffle_migration_order = output.index(output.detect{|o| /Copied migration 4_create_yaffles.rb from acts_as_yaffle/ =~ o }) + yaffle_migration_order = output.index(output.detect{|o| /Copied migration 4_create_yaffles.acts_as_yaffle.rb from acts_as_yaffle/ =~ o }) bukkits_migration_order = output.index(output.detect{|o| /NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/ =~ o }) assert_not_nil yaffle_migration_order, "Expected migration to be copied" assert_not_nil bukkits_migration_order, "Expected migration to be skipped" |