diff options
61 files changed, 1147 insertions, 367 deletions
diff --git a/actionmailer/lib/action_mailer/async.rb b/actionmailer/lib/action_mailer/async.rb deleted file mode 100644 index a364342745..0000000000 --- a/actionmailer/lib/action_mailer/async.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'delegate' - -module ActionMailer - module Async - def method_missing(method_name, *args) - if action_methods.include?(method_name.to_s) - QueuedMessage.new(queue, self, method_name, *args) - else - super - end - end - - def queue - Rails.queue - end - - class QueuedMessage < ::Delegator - attr_reader :queue - - def initialize(queue, mailer_class, method_name, *args) - @queue = queue - @mailer_class = mailer_class - @method_name = method_name - @args = args - end - - def __getobj__ - @actual_message ||= @mailer_class.send(:new, @method_name, *@args).message - end - - def run - __getobj__.deliver - end - - # Will push the message onto the Queue to be processed - def deliver - @queue << self - end - end - end -end
\ No newline at end of file diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 4a099553c0..26787c9b5e 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -1,4 +1,5 @@ require 'mail' +require 'action_mailer/queued_message' require 'action_mailer/collector' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/hash/except' @@ -362,6 +363,9 @@ module ActionMailer #:nodoc: # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with # <tt>delivery_method :test</tt>. Most useful for unit and functional testing. # + # * <tt>queue</> - The queue that will be used to deliver the mail. The queue should expect a job that + # responds to <tt>run</tt> + # class Base < AbstractController::Base include DeliveryMethods abstract! @@ -388,6 +392,8 @@ module ActionMailer #:nodoc: :parts_order => [ "text/plain", "text/enriched", "text/html" ] }.freeze + class_attribute :queue + class << self # Register one or more Observers which will be notified when mail is delivered. def register_observers(*observers) @@ -464,19 +470,6 @@ module ActionMailer #:nodoc: super || action_methods.include?(method.to_s) end - # Will force ActionMailer to push new messages to the queue defined - # in the ActionMailer class when set to true. - # - # class WelcomeMailer < ActionMailer::Base - # self.async = true - # end - def async=(truth) - if truth - require 'action_mailer/async' - extend ActionMailer::Async - end - end - protected def set_payload_for_mail(payload, mail) #:nodoc: @@ -491,9 +484,12 @@ module ActionMailer #:nodoc: payload[:mail] = mail.encoded end - def method_missing(method, *args) #:nodoc: - return super unless respond_to?(method) - new(method, *args).message + def method_missing(method_name, *args) + if action_methods.include?(method_name.to_s) + QueuedMessage.new(queue, self, method_name, *args) + else + super + end end end diff --git a/actionmailer/lib/action_mailer/queued_message.rb b/actionmailer/lib/action_mailer/queued_message.rb new file mode 100644 index 0000000000..e5868ab43b --- /dev/null +++ b/actionmailer/lib/action_mailer/queued_message.rb @@ -0,0 +1,27 @@ +require 'delegate' + +module ActionMailer + class QueuedMessage < ::Delegator + attr_reader :queue + + def initialize(queue, mailer_class, method_name, *args) + @queue = queue + @mailer_class = mailer_class + @method_name = method_name + @args = args + end + + def __getobj__ + @actual_message ||= @mailer_class.send(:new, @method_name, *@args).message + end + + def run + __getobj__.deliver + end + + # Will push the message onto the Queue to be processed + def deliver + @queue << self + end + end +end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 8679096735..1fa689de5b 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -19,6 +19,8 @@ module ActionMailer options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first + options.queue ||= app.queue + # make sure readers methods get compiled options.asset_path ||= app.config.asset_path options.asset_host ||= app.config.asset_host diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 99c44179fd..1167387def 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -11,6 +11,7 @@ end require 'minitest/autorun' require 'action_mailer' require 'action_mailer/test_case' +require 'rails/queueing' silence_warnings do # These external dependencies have warnings :/ @@ -26,6 +27,7 @@ ActionView::Template.register_template_handler :bak, lambda { |template| "Lame b FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__)) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH +ActionMailer::Base.queue = Rails::Queueing::SynchronousQueue.new class MockSMTP def self.deliveries diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index c7b8f47d89..862b954f9e 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -575,11 +575,11 @@ class BaseTest < ActiveSupport::TestCase end test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do - mail1 = ProcMailer.welcome + mail1 = ProcMailer.welcome['X-Proc-Method'] yesterday = 1.day.ago Time.stubs(:now).returns(yesterday) - mail2 = ProcMailer.welcome - assert(mail1['X-Proc-Method'].to_s.to_i > mail2['X-Proc-Method'].to_s.to_i) + mail2 = ProcMailer.welcome['X-Proc-Method'] + assert(mail1.to_s.to_i > mail2.to_s.to_i) end test "we can call other defined methods on the class as needed" do diff --git a/actionmailer/test/mailers/async_mailer.rb b/actionmailer/test/mailers/async_mailer.rb index ce601e7343..8a87e2e1cf 100644 --- a/actionmailer/test/mailers/async_mailer.rb +++ b/actionmailer/test/mailers/async_mailer.rb @@ -1,3 +1,2 @@ class AsyncMailer < BaseMailer - self.async = true end diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index 3d667f0a2f..38598a520c 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -10,6 +10,16 @@ require 'controller/fake_controllers' require 'action_mailer' ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH +class SynchronousQueue < Queue + def push(job) + job.run + end + alias << push + alias enq push +end + +ActionMailer::Base.queue = SynchronousQueue.new + class AssertSelectTest < ActionController::TestCase Assertion = ActiveSupport::TestCase::Assertion diff --git a/actionpack/test/fixtures/ruby_template.rb b/actionpack/test/fixtures/ruby_template.rb index d99833a24d..5097bce47c 100644 --- a/actionpack/test/fixtures/ruby_template.rb +++ b/actionpack/test/fixtures/ruby_template.rb @@ -1,3 +1,2 @@ body = "" body << ["Hello", "from", "Ruby", "code"].join(" ") -body diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb index cf1415b6c2..3d7067fbcb 100644 --- a/activemodel/lib/active_model/validations/clusivity.rb +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/range.rb' +require 'active_support/core_ext/range' module ActiveModel module Validations diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 6f7b2cb108..8c99a3929e 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,28 @@ ## Rails 4.0.0 (unreleased) ## +* You can now override the generated accessor methods for stored attributes + and reuse the original behavior with `read_store_attribute` and `write_store_attribute`, + which are counterparts to `read_attribute` and `write_attribute`. + + *Matt Jones* + +* Accept belongs_to (including polymorphic) association keys in queries + + The following queries are now equivalent: + + Post.where(:author => author) + Post.where(:author_id => author) + + PriceEstimate.where(:estimate_of => treasure) + PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure) + + *Peter Brown* + +* Use native `mysqldump` command instead of `structure_dump` method + when dumping the database structure to a sql file. Fixes #5547. + + *kennyj* + * Attribute predicate methods, such as `article.title?`, will now raise `ActiveModel::MissingAttributeError` if the attribute being queried for truthiness was not read from the database, instead of just returning false. @@ -21,7 +44,7 @@ *Jan Bernacki* -* Fix bug when call `store_accessor` multiple times. +* Fix bug when calling `store_accessor` multiple times. Fixes #7532. *Matt Jones* diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index ee8b816ef4..66132b7260 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -18,14 +18,8 @@ module ActiveRecord # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro. # - # This class has most of the basic instance methods removed, and delegates - # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a - # corner case, it even removes the +class+ method and that's why you get - # - # blog.posts.class # => Array - # - # though the object behind <tt>blog.posts</tt> is not an Array, but an - # ActiveRecord::Associations::HasManyAssociation. + # This class delegates unknown methods to <tt>@target</tt> via + # <tt>method_missing</tt>. # # The <tt>@target</tt> object is not \loaded until needed. For example, # 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 bf08459b3b..f42a5df75f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -5,14 +5,11 @@ require 'active_support/core_ext/module/deprecation' module ActiveRecord # Raised when a connection could not be obtained within the connection - # acquisition timeout period. + # acquisition timeout period: because max connections in pool + # are in use. class ConnectionTimeoutError < ConnectionNotEstablished end - # Raised when a connection pool is full and another connection is requested - class PoolFullError < ConnectionNotEstablished - end - module ConnectionAdapters # Connection pool base class for managing Active Record database # connections. @@ -187,7 +184,11 @@ module ActiveRecord return remove if any? elapsed = Time.now - t0 - raise ConnectionTimeoutError if elapsed >= timeout + if elapsed >= timeout + msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + [timeout, elapsed] + raise ConnectionTimeoutError, msg + end end ensure @num_waiting -= 1 @@ -350,12 +351,12 @@ module ActiveRecord # # If all connections are leased and the pool is at capacity (meaning the # number of currently leased connections is greater than or equal to the - # size limit set), an ActiveRecord::PoolFullError exception will be raised. + # size limit set), an ActiveRecord::ConnectionTimeoutError exception will be raised. # # Returns: an AbstractAdapter object. # # Raises: - # - PoolFullError: no connection can be obtained from the pool. + # - ConnectionTimeoutError: no connection can be obtained from the pool. def checkout synchronize do conn = acquire_connection @@ -416,22 +417,14 @@ module ActiveRecord # queue for a connection to become available. # # Raises: - # - PoolFullError if a connection could not be acquired (FIXME: - # why not ConnectionTimeoutError? + # - ConnectionTimeoutError if a connection could not be acquired def acquire_connection if conn = @available.poll conn elsif @connections.size < @size checkout_new_connection else - t0 = Time.now - begin - @available.poll(@checkout_timeout) - rescue ConnectionTimeoutError - msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % - [@checkout_timeout, Time.now - t0] - raise PoolFullError, msg - end + @available.poll(@checkout_timeout) end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 0390168461..816b5e17c1 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -114,7 +114,7 @@ module ActiveRecord case type when :string, :text then var_name - when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" + when :integer then "(#{var_name}.to_i)" when :float then "#{var_name}.to_f" when :decimal then "#{klass}.value_to_decimal(#{var_name})" when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index dd40351a38..b9a61f7d91 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -72,7 +72,7 @@ module ActiveRecord :port => config.port, :database => config.path.sub(%r{^/},""), :host => config.host } - spec.reject!{ |_,value| !value } + spec.reject!{ |_,value| value.blank? } if config.query options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys spec.merge!(options) diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb index 57553c29eb..44cde49bd5 100644 --- a/activerecord/lib/active_record/model.rb +++ b/activerecord/lib/active_record/model.rb @@ -101,9 +101,19 @@ module ActiveRecord def abstract_class? false end - + # Defines the name of the table column which will store the class name on single-table # inheritance situations. + # + # The default inheritance column name is +type+, which means it's a + # reserved word inside Active Record. To be able to use single-table + # inheritance with another column name, or to use the column +type+ in + # your own model for something else, you can override this method to + # return a different name: + # + # def self.inheritance_column + # 'zoink' + # end def inheritance_column 'type' end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index ae24542521..bcb26f72c8 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -18,9 +18,13 @@ db_namespace = namespace :db do end end - desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' + desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' task :create => [:load_config] do - ActiveRecord::Tasks::DatabaseTasks.create_current + if ENV['DATABASE_URL'] + ActiveRecord::Tasks::DatabaseTasks.create_database_url + else + ActiveRecord::Tasks::DatabaseTasks.create_current + end end namespace :drop do @@ -29,9 +33,13 @@ db_namespace = namespace :db do end end - desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)' + desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)' task :drop => [:load_config] do - ActiveRecord::Tasks::DatabaseTasks.drop_current + if ENV['DATABASE_URL'] + ActiveRecord::Tasks::DatabaseTasks.drop_database_url + else + ActiveRecord::Tasks::DatabaseTasks.drop_current + end end desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." @@ -88,8 +96,6 @@ db_namespace = namespace :db do desc 'Display status of migrations' task :status => [:environment, :load_config] do - config = ActiveRecord::Base.configurations[Rails.env] - ActiveRecord::Base.establish_connection(config) unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) puts 'Schema migrations table does not exist yet.' next # means "return" for rake task @@ -110,7 +116,7 @@ db_namespace = namespace :db do ['up', version, '********** NO FILE **********'] end # output - puts "\ndatabase: #{config['database']}\n\n" + puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration| @@ -186,7 +192,6 @@ db_namespace = namespace :db do task :load => [:environment, :load_config] do require 'active_record/fixtures' - ActiveRecord::Base.establish_connection(Rails.env) base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact @@ -225,7 +230,6 @@ db_namespace = namespace :db do require 'active_record/schema_dumper' filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" File.open(filename, "w:utf-8") do |file| - ActiveRecord::Base.establish_connection(Rails.env) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end db_namespace['schema:dump'].reenable @@ -277,22 +281,22 @@ db_namespace = namespace :db do desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' task :dump => [:environment, :load_config] do - abcs = ActiveRecord::Base.configurations filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") - case abcs[Rails.env]['adapter'] + current_config = ActiveRecord::Tasks::DatabaseTasks.current_config + case current_config['adapter'] when /mysql/, /postgresql/, /sqlite/ - ActiveRecord::Tasks::DatabaseTasks.structure_dump(abcs[Rails.env], filename) + ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) when 'oci', 'oracle' - ActiveRecord::Base.establish_connection(abcs[Rails.env]) + ActiveRecord::Base.establish_connection(current_config) File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } when 'sqlserver' - `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U` + `smoscript -s #{current_config['host']} -d #{current_config['database']} -u #{current_config['username']} -p #{current_config['password']} -f #{filename} -A -U` when "firebird" - set_firebird_env(abcs[Rails.env]) - db_string = firebird_db_string(abcs[Rails.env]) + set_firebird_env(current_config) + db_string = firebird_db_string(current_config) sh "isql -a #{db_string} > #{filename}" else - raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'" + raise "Task not supported by '#{current_config["adapter"]}'" end if ActiveRecord::Base.connection.supports_migrations? @@ -303,26 +307,24 @@ db_namespace = namespace :db do # desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do - env = ENV['RAILS_ENV'] || 'test' - - abcs = ActiveRecord::Base.configurations + current_config = ActiveRecord::Tasks::DatabaseTasks.current_config(:env => (ENV['RAILS_ENV'] || 'test')) filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") - case abcs[env]['adapter'] + case current_config['adapter'] when /mysql/, /postgresql/, /sqlite/ - ActiveRecord::Tasks::DatabaseTasks.structure_load(abcs[env], filename) + ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename) when 'sqlserver' - `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}` + `sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}` when 'oci', 'oracle' - ActiveRecord::Base.establish_connection(abcs[env]) + ActiveRecord::Base.establish_connection(current_config) IO.read(filename).split(";\n\n").each do |ddl| ActiveRecord::Base.connection.execute(ddl) end when 'firebird' - set_firebird_env(abcs[env]) - db_string = firebird_db_string(abcs[env]) + set_firebird_env(current_config) + db_string = firebird_db_string(current_config) sh "isql -i #{filename} #{db_string}" else - raise "Task not supported by '#{abcs[env]['adapter']}'" + raise "Task not supported by '#{current_config['adapter']}'" end end @@ -353,10 +355,10 @@ db_namespace = namespace :db do # desc "Recreate the test database from an existent structure.sql file" task :load_structure => 'db:test:purge' do begin - old_env, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], 'test' + ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test']) db_namespace["structure:load"].invoke ensure - ENV['RAILS_ENV'] = old_env + ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil) end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index cf949a893f..d9c65fa170 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -358,6 +358,10 @@ module ActiveRecord end end + def polymorphic? + options.key? :polymorphic + end + private def derive_class_name class_name = name.to_s.camelize diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index cb8f903474..36fc08e6ad 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,23 +1,51 @@ module ActiveRecord class PredicateBuilder # :nodoc: - def self.build_from_hash(engine, attributes, default_table) - attributes.map do |column, value| + def self.build_from_hash(klass, attributes, default_table) + queries = [] + + attributes.each do |column, value| table = default_table if value.is_a?(Hash) - table = Arel::Table.new(column, engine) - value.map { |k,v| build(table[k.to_sym], v) } + table = Arel::Table.new(column, default_table.engine) + association = klass.reflect_on_association(column.to_sym) + + value.each do |k, v| + queries.concat expand(association && association.klass, table, k, v) + end else column = column.to_s if column.include?('.') table_name, column = column.split('.', 2) - table = Arel::Table.new(table_name, engine) + table = Arel::Table.new(table_name, default_table.engine) end - build(table[column.to_sym], value) + queries.concat expand(klass, table, column, value) + end + end + + queries + end + + def self.expand(klass, table, column, value) + queries = [] + + # Find the foreign key when using queries such as: + # Post.where(:author => author) + # + # For polymorphic relationships, find the foreign key and type: + # PriceEstimate.where(:estimate_of => treasure) + if klass && value.class < Model::Tag && reflection = klass.reflect_on_association(column.to_sym) + if reflection.polymorphic? + queries << build(table[reflection.foreign_type], value.class.base_class) end - end.flatten + + column = reflection.foreign_key + end + + queries << build(table[column.to_sym], value) + queries end def self.references(attributes) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index f6bacf4822..3c59bd8a68 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -340,6 +340,24 @@ module ActiveRecord # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight }) # # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000') # + # In the case of a belongs_to relationship, an association key can be used + # to specify the model if an ActiveRecord object is used as the value. + # + # author = Author.find(1) + # + # # The following queries will be equivalent: + # Post.where(:author => author) + # Post.where(:author_id => author) + # + # This also works with polymorphic belongs_to relationships: + # + # treasure = Treasure.create(:name => 'gold coins') + # treasure.price_estimates << PriceEstimate.create(:price => 125) + # + # # The following queries will be equivalent: + # PriceEstimate.where(:estimate_of => treasure) + # PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure) + # # === Joins # # If the relation is the result of a join, you may create a condition which uses any of the @@ -690,7 +708,7 @@ module ActiveRecord [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) - PredicateBuilder.build_from_hash(table.engine, attributes, table) + PredicateBuilder.build_from_hash(klass, attributes, table) else [opts] end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 5c74c07ad1..42b4cff4b8 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -88,8 +88,8 @@ module ActiveRecord 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| + table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) + PredicateBuilder.build_from_hash(self.class, attrs, table).map { |b| connection.visitor.accept b }.join(' AND ') end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 8ea0ea239f..df7f58c81f 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -37,6 +37,28 @@ module ActiveRecord # The stored attribute names can be retrieved using +stored_attributes+. # # User.stored_attributes[:settings] # [:color, :homepage] + # + # == Overwriting default accessors + # + # All stored values are automatically available through accessors on the Active Record + # object, but sometimes you want to specialize this behavior. This can be done by overwriting + # the default accessors (using the same name as the attribute) and calling + # <tt>read_store_attribute(store_attribute_name, attr_name)</tt> and + # <tt>write_store_attribute(store_attribute_name, attr_name, value)</tt> to actually + # change things. + # + # class Song < ActiveRecord::Base + # # Uses a stored integer to hold the volume adjustment of the song + # store :settings, accessors: [:volume_adjustment] + # + # def volume_adjustment=(decibels) + # write_store_attribute(:settings, :volume_adjustment, decibels.to_i) + # end + # + # def volume_adjustment + # read_store_attribute(:settings, :volume_adjustment).to_i + # end + # end module Store extend ActiveSupport::Concern @@ -55,15 +77,11 @@ module ActiveRecord keys = keys.flatten keys.each do |key| define_method("#{key}=") do |value| - attribute = initialize_store_attribute(store_attribute) - if value != attribute[key] - send :"#{store_attribute}_will_change!" - attribute[key] = value - end + write_store_attribute(store_attribute, key, value) end define_method(key) do - initialize_store_attribute(store_attribute)[key] + read_store_attribute(store_attribute, key) end end @@ -72,6 +90,20 @@ module ActiveRecord end end + protected + def read_store_attribute(store_attribute, key) + attribute = initialize_store_attribute(store_attribute) + attribute[key] + end + + def write_store_attribute(store_attribute, key, value) + attribute = initialize_store_attribute(store_attribute) + if value != attribute[key] + send :"#{store_attribute}_will_change!" + attribute[key] = value + end + end + private def initialize_store_attribute(store_attribute) attribute = send(store_attribute) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index b41cc68b6a..fda51b3d76 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -3,6 +3,8 @@ module ActiveRecord module DatabaseTasks # :nodoc: extend self + attr_writer :current_config + LOCAL_HOSTS = ['127.0.0.1', 'localhost'] def register_task(pattern, task) @@ -14,6 +16,19 @@ module ActiveRecord register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + def current_config(options = {}) + options.reverse_merge! :env => Rails.env + if options.has_key?(:config) + @current_config = options[:config] + else + @current_config ||= if ENV['DATABASE_URL'] + database_url_config + else + ActiveRecord::Base.configurations[options[:env]] + end + end + end + def create(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).create @@ -33,6 +48,10 @@ module ActiveRecord ActiveRecord::Base.establish_connection environment end + def create_database_url + create database_url_config + end + def drop(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).drop @@ -51,6 +70,10 @@ module ActiveRecord } end + def drop_database_url + drop database_url_config + end + def charset_current(environment = Rails.env) charset ActiveRecord::Base.configurations[environment] end @@ -87,6 +110,11 @@ module ActiveRecord private + def database_url_config + @database_url_config ||= + ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys + end + def class_for_adapter(adapter) key = @tasks.keys.detect { |pattern| adapter[pattern] } @tasks[key] diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 85d08402f9..2340f949b7 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -27,7 +27,7 @@ module ActiveRecord rescue error_class => error $stderr.puts error.error $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" - $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['charset'] + $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding'] end def drop @@ -49,19 +49,17 @@ module ActiveRecord end def structure_dump(filename) - establish_connection configuration - File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } + args = prepare_command_options('mysqldump') + args.concat(["--result-file", "#{filename}"]) + args.concat(["--no-data"]) + args.concat(["#{configuration['database']}"]) + Kernel.system(*args) end def structure_load(filename) - args = ['mysql'] - args.concat(['--user', configuration['username']]) if configuration['username'] - args << "--password=#{configuration['password']}" if configuration['password'] - args.concat(['--default-character-set', configuration['charset']]) if configuration['charset'] - configuration.slice('host', 'port', 'socket', 'database').each do |k, v| - args.concat([ "--#{k}", v ]) if v - end + args = prepare_command_options('mysql') args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) + args.concat(["--database", "#{configuration['database']}"]) Kernel.system(*args) end @@ -77,7 +75,7 @@ module ActiveRecord def creation_options { - charset: (configuration['charset'] || DEFAULT_CHARSET), + charset: (configuration['encoding'] || DEFAULT_CHARSET), collation: (configuration['collation'] || DEFAULT_COLLATION) } end @@ -113,6 +111,18 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; $stdout.print "Please provide the root password for your mysql installation\n>" $stdin.gets.strip end + + def prepare_command_options(command) + args = [command] + args.concat(['--user', configuration['username']]) if configuration['username'] + args << "--password=#{configuration['password']}" if configuration['password'] + args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding'] + configuration.slice('host', 'port', 'socket').each do |k, v| + args.concat([ "--#{k}", v ]) if v + end + args + end + end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 8287b35aaf..0718d0886f 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -89,7 +89,7 @@ module ActiveRecord end def test_full_pool_exception - assert_raises(PoolFullError) do + assert_raises(ConnectionTimeoutError) do (@pool.size + 1).times do @pool.checkout end diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 673a2b2b88..434d2b7ba5 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -13,7 +13,6 @@ module ActiveRecord spec = resolve 'mysql://foo?encoding=utf8' assert_equal({ :adapter => "mysql", - :database => "", :host => "foo", :encoding => "utf8" }, spec) end @@ -33,7 +32,6 @@ module ActiveRecord spec = resolve 'mysql://foo:123?encoding=utf8' assert_equal({ :adapter => "mysql", - :database => "", :port => 123, :host => "foo", :encoding => "utf8" }, spec) diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 90c690e266..3163cf79ad 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -1,10 +1,71 @@ require "cases/helper" +require 'models/author' +require 'models/price_estimate' +require 'models/treasure' require 'models/post' +require 'models/comment' module ActiveRecord class WhereTest < ActiveRecord::TestCase fixtures :posts + def test_belongs_to_shallow_where + author = Author.new + author.id = 1 + + assert_equal Post.where(author_id: 1).to_sql, Post.where(author: author).to_sql + end + + def test_belongs_to_nested_where + parent = Comment.new + parent.id = 1 + + expected = Post.where(comments: { parent_id: 1 }).joins(:comments) + actual = Post.where(comments: { parent: parent }).joins(:comments) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_polymorphic_shallow_where + treasure = Treasure.new + treasure.id = 1 + + expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1) + actual = PriceEstimate.where(estimate_of: treasure) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_polymorphic_sti_shallow_where + treasure = HiddenTreasure.new + treasure.id = 1 + + expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1) + actual = PriceEstimate.where(estimate_of: treasure) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_polymorphic_nested_where + thing = Post.new + thing.id = 1 + + expected = Treasure.where(price_estimates: { thing_type: 'Post', thing_id: 1 }).joins(:price_estimates) + actual = Treasure.where(price_estimates: { thing: thing }).joins(:price_estimates) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_polymorphic_sti_nested_where + treasure = HiddenTreasure.new + treasure.id = 1 + + expected = Treasure.where(price_estimates: { estimate_of_type: 'Treasure', estimate_of_id: 1 }).joins(:price_estimates) + actual = Treasure.where(price_estimates: { estimate_of: treasure }).joins(:price_estimates) + + assert_equal expected.to_sql, actual.to_sql + end + def test_where_error assert_raises(ActiveRecord::StatementInvalid) do Post.where(:id => { 'posts.author_id' => 10 }).first diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 2741f223da..dc47d40f41 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -29,6 +29,12 @@ class StoreTest < ActiveRecord::TestCase assert_equal 'graeters', @john.reload.settings[:icecream] end + test "overriding a read accessor" do + @john.settings[:phone_number] = '1234567890' + + assert_equal '(123) 456-7890', @john.phone_number + end + test "updating the store will mark it as changed" do @john.color = 'red' assert @john.settings_changed? @@ -54,6 +60,12 @@ class StoreTest < ActiveRecord::TestCase assert_equal false, @john.remember_login end + test "overriding a write accessor" do + @john.phone_number = '(123) 456-7890' + + assert_equal '1234567890', @john.settings[:phone_number] + end + test "preserve store attributes data in HashWithIndifferentAccess format without any conversion" do @john.json_data = HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy') @john.height = 'low' @@ -124,7 +136,7 @@ class StoreTest < ActiveRecord::TestCase end test "all stored attributes are returned" do - assert_equal [:color, :homepage, :favorite_food], Admin::User.stored_attributes[:settings] + assert_equal [:color, :homepage, :favorite_food, :phone_number], Admin::User.stored_attributes[:settings] end test "stores_attributes are class level settings" do diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index be591da8d6..46b97a1274 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -32,7 +32,7 @@ module ActiveRecord with('my-app-db', {:charset => 'latin', :collation => 'latin_ci'}) ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge( - 'charset' => 'latin', 'collation' => 'latin_ci' + 'encoding' => 'latin', 'collation' => 'latin_ci' ) end @@ -176,7 +176,7 @@ module ActiveRecord with('test-db', {:charset => 'latin', :collation => 'latin_ci'}) ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge( - 'charset' => 'latin', 'collation' => 'latin_ci' + 'encoding' => 'latin', 'collation' => 'latin_ci' ) end end @@ -219,44 +219,31 @@ module ActiveRecord class MySQLStructureDumpTest < ActiveRecord::TestCase def setup - @connection = stub(:structure_dump => true) @configuration = { 'adapter' => 'mysql', 'database' => 'test-db' } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_structure_dump filename = "awesome-file.sql" - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - @connection.expects(:structure_dump) + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db") ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - assert File.exists?(filename) - ensure - FileUtils.rm(filename) end end class MySQLStructureLoadTest < ActiveRecord::TestCase def setup - @connection = stub @configuration = { 'adapter' => 'mysql', 'database' => 'test-db' } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - Kernel.stubs(:system) end def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with('mysql', '--database', 'test-db', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}) + Kernel.expects(:system).with('mysql', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db") ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index 6c4eb03b06..35170faa76 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -1,8 +1,16 @@ class Admin::User < ActiveRecord::Base belongs_to :account store :settings, :accessors => [ :color, :homepage ] - store_accessor :settings, :favorite_food + store_accessor :settings, :favorite_food, :phone_number store :preferences, :accessors => [ :remember_login ] store :json_data, :accessors => [ :height, :weight ], :coder => JSON store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => JSON + + def phone_number + read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/,'(\1) \2-\3') + end + + def phone_number=(value) + write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/,'')) + end end diff --git a/activerecord/test/models/price_estimate.rb b/activerecord/test/models/price_estimate.rb index ef3bba36a9..d09e2a88a3 100644 --- a/activerecord/test/models/price_estimate.rb +++ b/activerecord/test/models/price_estimate.rb @@ -1,3 +1,4 @@ class PriceEstimate < ActiveRecord::Base belongs_to :estimate_of, :polymorphic => true + belongs_to :thing, polymorphic: true end diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb index 2a98e74f2c..e864295acf 100644 --- a/activerecord/test/models/treasure.rb +++ b/activerecord/test/models/treasure.rb @@ -6,3 +6,6 @@ class Treasure < ActiveRecord::Base accepts_nested_attributes_for :looter end + +class HiddenTreasure < Treasure +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index b4e611cb09..798ea20efc 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -693,6 +693,7 @@ ActiveRecord::Schema.define do create_table :treasures, :force => true do |t| t.column :name, :string + t.column :type, :string t.column :looter_id, :integer t.column :looter_type, :string end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 3914f4dc4c..c36df796a8 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,39 @@ ## Rails 4.0.0 (unreleased) ## +* An optional block can be passed to `Hash#deep_merge`. The block will be invoked for each duplicated key + and used to resolve the conflict. *Pranas Kiziela* + +* ActiveSupport::Deprecation is now a class. It is possible to create an instance + of deprecator. Backwards compatibility has been preserved. + + You can choose which instance of the deprecator will be used. + + deprecate :method_name, :deprecator => deprecator_instance + + You can use ActiveSupport::Deprecation in your gem. + + require 'active_support/deprecation' + require 'active_support/core_ext/module/deprecation' + + class MyGem + def self.deprecator + ActiveSupport::Deprecation.new('2.0', 'MyGem') + end + + def old_method + end + + def new_method + end + + deprecate :old_method => :new_method, :deprecator => deprecator + end + + MyGem.new.old_method + # => DEPRECATION WARNING: old_method is deprecated and will be removed from MyGem 2.0 (use new_method instead). (called from <main> at file.rb:18) + + *Piotr Niełacny & Robert Pankowecki* + * `ERB::Util.html_escape` encodes single quote as `#39`. Decimal form has better support in old browsers. *Kalys Osmonov* * `ActiveSupport::Callbacks`: deprecate monkey patch of object callbacks. diff --git a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb index 023bf68a87..22acedcf81 100644 --- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb +++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb @@ -6,15 +6,21 @@ class Hash # # h1.deep_merge(h2) #=> {:x => {:y => [7, 8, 9]}, :z => "xyz"} # h2.deep_merge(h1) #=> {:x => {:y => [4, 5, 6]}, :z => [7, 8, 9]} - def deep_merge(other_hash) - dup.deep_merge!(other_hash) + # h1.deep_merge(h2) { |key, old, new| Array.wrap(old) + Array.wrap(new) } + # #=> {:x => {:y => [4, 5, 6, 7, 8, 9]}, :z => [7, 8, 9, "xyz"]} + def deep_merge(other_hash, &block) + dup.deep_merge!(other_hash, &block) end # Same as +deep_merge+, but modifies +self+. - def deep_merge!(other_hash) + def deep_merge!(other_hash, &block) other_hash.each_pair do |k,v| tv = self[k] - self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v + if tv.is_a?(Hash) && v.is_a?(Hash) + self[k] = tv.deep_merge(v, &block) + else + self[k] = block && tv ? block.call(k, tv, v) : v + end end self end diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb index 9e77ac3c45..34ec6a3d8f 100644 --- a/activesupport/lib/active_support/core_ext/module/deprecation.rb +++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb @@ -1,10 +1,24 @@ require 'active_support/deprecation/method_wrappers' class Module - # Declare that a method has been deprecated. # deprecate :foo # deprecate :bar => 'message' # deprecate :foo, :bar, :baz => 'warning!', :qux => 'gone!' + # + # You can also use custom deprecator instance: + # + # deprecate :foo, :deprecator => MyLib::Deprecator.new + # deprecate :foo, :bar => "warning!", :deprecator => MyLib::Deprecator.new + # + # \Custom deprecators must respond to <tt>deprecation_warning(deprecated_method_name, message, caller_backtrace)</tt> + # method where you can implement your custom warning behavior. + # + # class MyLib::Deprecator + # def deprecation_warning(deprecated_method_name, message, caller_backtrace) + # message = "#{method_name} is deprecated and will be removed from MyLibrary | #{message}" + # Kernel.warn message + # end + # end def deprecate(*method_names) ActiveSupport::Deprecation.deprecate_methods(self, *method_names) end diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index e3b4a7240e..b3f5fde335 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -1,19 +1,34 @@ require 'active_support/core_ext/module/deprecation' +require 'active_support/deprecation/instance_delegator' require 'active_support/deprecation/behaviors' require 'active_support/deprecation/reporting' require 'active_support/deprecation/method_wrappers' require 'active_support/deprecation/proxy_wrappers' +require 'singleton' module ActiveSupport - module Deprecation - class << self - # The version the deprecated behavior will be removed, by default. - attr_accessor :deprecation_horizon - end - self.deprecation_horizon = '4.1' + # \Deprecation specifies the API used by Rails to deprecate methods, instance + # variables, objects and constants. + class Deprecation + include Singleton + include InstanceDelegator + include Behavior + include Reporting + include MethodWrapper + + # The version the deprecated behavior will be removed, by default. + attr_accessor :deprecation_horizon - # By default, warnings are not silenced and debugging is off. - self.silenced = false - self.debug = false + # It accepts two parameters on initialization. The first is an version of library + # and the second is an library name + # + # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') + def initialize(deprecation_horizon = '4.1', gem_name = 'Rails') + self.gem_name = gem_name + self.deprecation_horizon = deprecation_horizon + # By default, warnings are not silenced and debugging is off. + self.silenced = false + self.debug = false + end end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index fc962dcb57..b4c8a0e92d 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -1,8 +1,32 @@ require "active_support/notifications" module ActiveSupport - module Deprecation - class << self + class Deprecation + # Default warning behaviors per Rails.env. + DEFAULT_BEHAVIORS = { + :stderr => Proc.new { |message, callstack| + $stderr.puts(message) + $stderr.puts callstack.join("\n ") if debug + }, + :log => Proc.new { |message, callstack| + logger = + if defined?(Rails) && Rails.logger + Rails.logger + else + require 'active_support/logger' + ActiveSupport::Logger.new($stderr) + end + logger.warn message + logger.debug callstack.join("\n ") if debug + }, + :notify => Proc.new { |message, callstack| + ActiveSupport::Notifications.instrument("deprecation.rails", + :message => message, :callstack => callstack) + }, + :silence => Proc.new { |message, callstack| } + } + + module Behavior # Whether to print a backtrace along with the warning. attr_accessor :debug @@ -16,9 +40,9 @@ module ActiveSupport # # Available behaviors: # - # [+stderr+] Log all deprecation warnings to <tt>$stderr</tt>. + # [+stderr+] Log all deprecation warnings to +$stderr+. # [+log+] Log all deprecation warnings to +Rails.logger+. - # [+notify+] Use <tt>ActiveSupport::Notifications</tt> to notify +deprecation.rails+. + # [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+. # [+silence+] Do nothing. # # Setting behaviors only affects deprecations that happen after boot time. @@ -28,36 +52,12 @@ module ActiveSupport # ActiveSupport::Deprecation.behavior = :stderr # ActiveSupport::Deprecation.behavior = [:stderr, :log] # ActiveSupport::Deprecation.behavior = MyCustomHandler - # ActiveSupport::Deprecation.behavior = proc { |message, callstack| + # ActiveSupport::Deprecation.behavior = proc { |message, callstack| # # custom stuff # } def behavior=(behavior) @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || b } end end - - # Default warning behaviors per Rails.env. - DEFAULT_BEHAVIORS = { - :stderr => Proc.new { |message, callstack| - $stderr.puts(message) - $stderr.puts callstack.join("\n ") if debug - }, - :log => Proc.new { |message, callstack| - logger = - if defined?(Rails) && Rails.logger - Rails.logger - else - require 'active_support/logger' - ActiveSupport::Logger.new($stderr) - end - logger.warn message - logger.debug callstack.join("\n ") if debug - }, - :notify => Proc.new { |message, callstack| - ActiveSupport::Notifications.instrument("deprecation.rails", - :message => message, :callstack => callstack) - }, - :silence => Proc.new { |message, callstack| } - } end end diff --git a/activesupport/lib/active_support/deprecation/instance_delegator.rb b/activesupport/lib/active_support/deprecation/instance_delegator.rb new file mode 100644 index 0000000000..ff240cb887 --- /dev/null +++ b/activesupport/lib/active_support/deprecation/instance_delegator.rb @@ -0,0 +1,24 @@ +require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/module/delegation' + +module ActiveSupport + class Deprecation + module InstanceDelegator + def self.included(base) + base.extend(ClassMethods) + base.public_class_method :new + end + + module ClassMethods + def include(included_module) + included_module.instance_methods.each { |m| method_added(m) } + super + end + + def method_added(method_name) + singleton_class.delegate(method_name, to: :instance) + end + end + end + end +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index 257b70e34a..d3907b03e5 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -2,45 +2,41 @@ require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/array/extract_options' module ActiveSupport - module Deprecation - # Declare that a method has been deprecated. - # - # module Fred - # extend self - # - # def foo; end - # def bar; end - # def baz; end - # end - # - # ActiveSupport::Deprecation.deprecate_methods(Fred, :foo, bar: :qux, baz: 'use Bar#baz instead') - # # => [:foo, :bar, :baz] - # - # Fred.foo - # # => "DEPRECATION WARNING: foo is deprecated and will be removed from Rails 4.1." - # - # Fred.bar - # # => "DEPRECATION WARNING: bar is deprecated and will be removed from Rails 4.1 (use qux instead)." - # - # Fred.baz - # # => "DEPRECATION WARNING: baz is deprecated and will be removed from Rails 4.1 (use Bar#baz instead)." - def self.deprecate_methods(target_module, *method_names) - options = method_names.extract_options! - method_names += options.keys + class Deprecation + module MethodWrapper + # Declare that a method has been deprecated. + # + # module Fred + # extend self + # + # def foo; end + # def bar; end + # def baz; end + # end + # + # ActiveSupport::Deprecation.deprecate_methods(Fred, :foo, bar: :qux, baz: 'use Bar#baz instead') + # # => [:foo, :bar, :baz] + # + # Fred.foo + # # => "DEPRECATION WARNING: foo is deprecated and will be removed from Rails 4.1." + # + # Fred.bar + # # => "DEPRECATION WARNING: bar is deprecated and will be removed from Rails 4.1 (use qux instead)." + # + # Fred.baz + # # => "DEPRECATION WARNING: baz is deprecated and will be removed from Rails 4.1 (use Bar#baz instead)." + def deprecate_methods(target_module, *method_names) + options = method_names.extract_options! + deprecator = options.delete(:deprecator) || ActiveSupport::Deprecation.instance + method_names += options.keys - method_names.each do |method_name| - target_module.alias_method_chain(method_name, :deprecation) do |target, punctuation| - target_module.module_eval(<<-end_eval, __FILE__, __LINE__ + 1) - def #{target}_with_deprecation#{punctuation}(*args, &block) - ::ActiveSupport::Deprecation.warn( - ::ActiveSupport::Deprecation.deprecated_method_warning( - :#{method_name}, - #{options[method_name].inspect}), - caller - ) - send(:#{target}_without_deprecation#{punctuation}, *args, &block) + method_names.each do |method_name| + target_module.alias_method_chain(method_name, :deprecation) do |target, punctuation| + target_module.send(:define_method, "#{target}_with_deprecation#{punctuation}") do |*args, &block| + deprecator.deprecation_warning(method_name, options[method_name], caller) + send(:"#{target}_without_deprecation#{punctuation}", *args, &block) end - end_eval + end end end end diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index a65fcafb44..17e69c34a5 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -1,7 +1,7 @@ require 'active_support/inflector/methods' module ActiveSupport - module Deprecation + class Deprecation class DeprecationProxy #:nodoc: def self.new(*args, &block) object = args.first @@ -25,10 +25,20 @@ module ActiveSupport end end - class DeprecatedObjectProxy < DeprecationProxy #:nodoc: - def initialize(object, message) + # This DeprecatedObjectProxy transforms object to depracated object. + # + # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!") + # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!", deprecator_instance) + # + # When someone execute any method expect +inspect+ on proxy object this will + # trigger +warn+ method on +deprecator_instance+. + # + # Default deprecator is <tt>ActiveSupport::Deprecation</tt> + class DeprecatedObjectProxy < DeprecationProxy + def initialize(object, message, deprecator = ActiveSupport::Deprecation.instance) @object = object @message = message + @deprecator = deprecator end private @@ -37,15 +47,40 @@ module ActiveSupport end def warn(callstack, called, args) - ActiveSupport::Deprecation.warn(@message, callstack) + @deprecator.warn(@message, callstack) end end - # Stand-in for <tt>@request</tt>, <tt>@attributes</tt>, <tt>@params</tt>, etc. - # which emits deprecation warnings on any method call (except +inspect+). - class DeprecatedInstanceVariableProxy < DeprecationProxy #:nodoc: - def initialize(instance, method, var = "@#{method}") - @instance, @method, @var = instance, method, var + # This DeprecatedInstanceVariableProxy transforms instance variable to + # depracated instance variable. + # + # class Example + # def initialize(deprecator) + # @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request, deprecator) + # @_request = :a_request + # end + # + # def request + # @_request + # end + # + # def old_request + # @request + # end + # end + # + # When someone execute any method on @request variable this will trigger + # +warn+ method on +deprecator_instance+ and will fetch <tt>@_request</tt> + # variable via +request+ method and execute the same method on non-proxy + # instance variable. + # + # Default deprecator is <tt>ActiveSupport::Deprecation</tt>. + class DeprecatedInstanceVariableProxy < DeprecationProxy + def initialize(instance, method, var = "@#{method}", deprecator = ActiveSupport::Deprecation.instance) + @instance = instance + @method = method + @var = var + @deprecator = deprecator end private @@ -54,14 +89,24 @@ module ActiveSupport end def warn(callstack, called, args) - ActiveSupport::Deprecation.warn("#{@var} is deprecated! Call #{@method}.#{called} instead of #{@var}.#{called}. Args: #{args.inspect}", callstack) + @deprecator.warn("#{@var} is deprecated! Call #{@method}.#{called} instead of #{@var}.#{called}. Args: #{args.inspect}", callstack) end end - class DeprecatedConstantProxy < DeprecationProxy #:nodoc:all - def initialize(old_const, new_const) + # This DeprecatedConstantProxy transforms constant to depracated constant. + # + # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST') + # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST', deprecator_instance) + # + # When someone use old constant this will trigger +warn+ method on + # +deprecator_instance+. + # + # Default deprecator is <tt>ActiveSupport::Deprecation</tt>. + class DeprecatedConstantProxy < DeprecationProxy + def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance) @old_const = old_const @new_const = new_const + @deprecator = deprecator end def class @@ -74,7 +119,7 @@ module ActiveSupport end def warn(callstack, called, args) - ActiveSupport::Deprecation.warn("#{@old_const} is deprecated! Use #{@new_const} instead.", callstack) + @deprecator.warn("#{@old_const} is deprecated! Use #{@new_const} instead.", callstack) end end end diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index a1e9618084..02e8ff7c87 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -1,7 +1,10 @@ module ActiveSupport - module Deprecation - class << self + class Deprecation + module Reporting + # Whether to print a message (silent mode) attr_accessor :silenced + # Name of gem where method is deprecated + attr_accessor :gem_name # Outputs a deprecation warning to the output configured by # <tt>ActiveSupport::Deprecation.behavior</tt>. @@ -31,16 +34,30 @@ module ActiveSupport @silenced = old_silenced end - def deprecated_method_warning(method_name, message = nil) - warning = "#{method_name} is deprecated and will be removed from Rails #{deprecation_horizon}" - case message - when Symbol then "#{warning} (use #{message} instead)" - when String then "#{warning} (#{message})" - else warning + def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = caller) + deprecated_method_warning(deprecated_method_name, message).tap do |message| + warn(message, caller_backtrace) end end private + # Outputs a deprecation warning message + # + # ActiveSupport::Deprecation.deprecated_method_warning(:method_name) + # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon}" + # ActiveSupport::Deprecation.deprecated_method_warning(:method_name, :another_method) + # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (use another_method instead)" + # ActiveSupport::Deprecation.deprecated_method_warning(:method_name, "Optional message") + # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (Optional message)" + def deprecated_method_warning(method_name, message = nil) + warning = "#{method_name} is deprecated and will be removed from #{gem_name} #{deprecation_horizon}" + case message + when Symbol then "#{warning} (use #{message} instead)" + when String then "#{warning} (#{message})" + else warning + end + end + def deprecation_message(callstack, message = nil) message ||= "You are using deprecated behavior which will be removed from the next major or minor release." message += '.' unless message =~ /\.$/ diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 94463cc311..01934dd2c3 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -582,6 +582,16 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, hash_1 end + def test_deep_merge_with_block + hash_1 = { :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } } + hash_2 = { :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } } + expected = { :a => [:a, "a", 1], :b => "b", :c => { :c1 => [:c1, "c1", 2], :c2 => "c2", :c3 => { :d1 => "d1", :d2 => "d2" } } } + assert_equal(expected, hash_1.deep_merge(hash_2) { |k,o,n| [k, o, n] }) + + hash_1.deep_merge!(hash_2) { |k,o,n| [k, o, n] } + assert_equal expected, hash_1 + end + def test_deep_merge_on_indifferent_access hash_1 = HashWithIndifferentAccess.new({ :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } }) hash_2 = HashWithIndifferentAccess.new({ :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } }) diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index e21f3efe36..c081103cc7 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -104,6 +104,17 @@ class DeprecationTest < ActiveSupport::TestCase assert_match(/call stack!/, content) end + def test_default_stderr_behavior_with_warn_method + ActiveSupport::Deprecation.behavior = :stderr + + content = capture(:stderr) { + ActiveSupport::Deprecation.warn('Instance error!', ['instance call stack!']) + } + + assert_match(/Instance error!/, content) + assert_match(/instance call stack!/, content) + end + def test_default_silence_behavior ActiveSupport::Deprecation.behavior = :silence behavior = ActiveSupport::Deprecation.behavior.first @@ -186,4 +197,142 @@ class DeprecationTest < ActiveSupport::TestCase def test_deprecation_with_explicit_message assert_deprecated(/you now need to do something extra for this one/) { @dtc.d } end + + def test_deprecation_in_other_object + messages = [] + + klass = Class.new do + delegate :warn, :behavior=, to: ActiveSupport::Deprecation + end + + o = klass.new + o.behavior = Proc.new { |message, callstack| messages << message } + assert_difference("messages.size") do + o.warn("warning") + end + end + + def test_deprecated_method_with_custom_method_warning + deprecator = deprecator_with_messages + + class << deprecator + private + def deprecated_method_warning(method, message) + "deprecator.deprecated_method_warning.#{method}" + end + end + + deprecatee = Class.new do + def method + end + deprecate :method, deprecator: deprecator + end + + deprecatee.new.method + assert deprecator.messages.first.match("DEPRECATION WARNING: deprecator.deprecated_method_warning.method") + end + + def test_deprecate_with_custom_deprecator + custom_deprecator = mock('Deprecator') do + expects(:deprecation_warning) + end + + klass = Class.new do + def method + end + deprecate :method, deprecator: custom_deprecator + end + + klass.new.method + end + + def test_deprecated_constant_with_deprecator_given + deprecator = deprecator_with_messages + klass = Class.new + klass.const_set(:OLD, ActiveSupport::Deprecation::DeprecatedConstantProxy.new('klass::OLD', 'Object', deprecator) ) + assert_difference("deprecator.messages.size") do + klass::OLD.to_s + end + end + + def test_deprecated_instance_variable_with_instance_deprecator + deprecator = deprecator_with_messages + + klass = Class.new() do + def initialize(deprecator) + @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request, deprecator) + @_request = :a_request + end + def request; @_request end + def old_request; @request end + end + + assert_difference("deprecator.messages.size") { klass.new(deprecator).old_request.to_s } + end + + def test_deprecated_instance_variable_with_given_deprecator + deprecator = deprecator_with_messages + + klass = Class.new do + define_method(:initialize) do + @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request, deprecator) + @_request = :a_request + end + def request; @_request end + def old_request; @request end + end + + assert_difference("deprecator.messages.size") { klass.new.old_request.to_s } + end + + def test_delegate_deprecator_instance + klass = Class.new do + attr_reader :last_message + delegate :warn, :behavior=, to: ActiveSupport::Deprecation + + def initialize + self.behavior = [Proc.new { |message| @last_message = message }] + end + + def deprecated_method + warn(deprecated_method_warning(:deprecated_method, "You are calling deprecated method")) + end + + private + def deprecated_method_warning(method_name, message = nil) + message || "#{method_name} is deprecated and will be removed from This Library" + end + end + + object = klass.new + object.deprecated_method + assert_match(/You are calling deprecated method/, object.last_message) + end + + def test_default_gem_name + deprecator = ActiveSupport::Deprecation.new + + deprecator.send(:deprecated_method_warning, :deprecated_method, "You are calling deprecated method").tap do |message| + assert_match(/is deprecated and will be removed from Rails/, message) + end + end + + def test_custom_gem_name + deprecator = ActiveSupport::Deprecation.new('2.0', 'Custom') + + deprecator.send(:deprecated_method_warning, :deprecated_method, "You are calling deprecated method").tap do |message| + assert_match(/is deprecated and will be removed from Custom/, message) + end + end + + private + def deprecator_with_messages + klass = Class.new(ActiveSupport::Deprecation) + deprecator = klass.new + deprecator.behavior = Proc.new{|message, callstack| deprecator.messages << message} + def deprecator.messages + @messages ||= [] + end + deprecator + end end diff --git a/guides/source/action_mailer_basics.textile b/guides/source/action_mailer_basics.textile index 5f09b8e410..d92d5c48fb 100644 --- a/guides/source/action_mailer_basics.textile +++ b/guides/source/action_mailer_basics.textile @@ -472,7 +472,6 @@ The following configuration options are best made in one of the environment file |+delivery_method+|Defines a delivery method. Possible values are <tt>:smtp</tt> (default), <tt>:sendmail</tt>, <tt>:file</tt> and <tt>:test</tt>.| |+perform_deliveries+|Determines whether deliveries are actually carried out when the +deliver+ method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing.| |+deliveries+|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.| -|+async+|Setting this flag will turn on asynchronous message sending, message rendering and delivery will be pushed to <tt>Rails.queue</tt> for processing.| |+default_options+|Allows you to set default values for the <tt>mail</tt> method options (<tt>:from</tt>, <tt>:reply_to</tt>, etc.).| h4. Example Action Mailer Configuration @@ -535,30 +534,22 @@ In the test we send the email and store the returned object in the +email+ varia h3. Asynchronous -You can turn on application-wide asynchronous message sending by adding to your <tt>config/application.rb</tt> file: +Rails provides a Synchronous Queue by default. If you want to use an Asynchronous one you will need to configure an async Queue provider like Resque. Queue providers are supposed to have a Railtie where they configure it's own async queue. -<ruby> -config.action_mailer.async = true -</ruby> +h4. Custom Queues -Alternatively you can turn on async within specific mailers: +If you need a different queue than <tt>Rails.queue</tt> for your mailer you can use <tt>ActionMailer::Base.queue=</tt>: <ruby> class WelcomeMailer < ActionMailer::Base - self.async = true + self.queue = MyQueue.new end </ruby> -h4. Custom Queues - -If you need a different queue than <tt>Rails.queue</tt> for your mailer you can override <tt>ActionMailer::Base#queue</tt>: +or adding to your <tt>config/environments/$RAILS_ENV.rb</tt>: <ruby> -class WelcomeMailer < ActionMailer::Base - def queue - MyQueue.new - end -end +config.action_mailer.queue = MyQueue.new </ruby> Your custom queue should expect a job that responds to <tt>#run</tt>. diff --git a/guides/source/active_record_querying.textile b/guides/source/active_record_querying.textile index 96ae5b2972..e21b86721f 100644 --- a/guides/source/active_record_querying.textile +++ b/guides/source/active_record_querying.textile @@ -465,6 +465,13 @@ The field name can also be a string: Client.where('locked' => true) </ruby> +In the case of a belongs_to relationship, an association key can be used to specify the model if an ActiveRecord object is used as the value. This method works with polymorphic relationships as well. + +<ruby> +Post.where(:author => author) +Author.joins(:posts).where(:posts => {:author => author}) +</ruby> + NOTE: The values cannot be symbols. For example, you cannot do +Client.where(:status => :active)+. h5(#hash-range_conditions). Range Conditions diff --git a/guides/source/configuring.textile b/guides/source/configuring.textile index dd84754b22..73d6102eac 100644 --- a/guides/source/configuring.textile +++ b/guides/source/configuring.textile @@ -434,6 +434,11 @@ config.action_mailer.observers = ["MailObserver"] config.action_mailer.interceptors = ["MailInterceptor"] </ruby> +* +config.action_mailer.queue+ registers the queue that will be used to deliver the mail. +<ruby> +config.action_mailer.queue = SomeQueue.new +</ruby> + h4. Configuring Active Support There are a few configuration options available in Active Support: diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index e889f0c23c..1c28ac7476 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,5 +1,17 @@ ## Rails 4.0.0 (unreleased) ## +* Change `rails new` and `rails plugin new` generators to name the `.gitkeep` files + as `.keep` in a more SCM-agnostic way. + + Change `--skip-git` option to only skip the `.gitignore` file and still generate + the `.keep` files. + + Add `--skip-keeps` option to skip the `.keep` files. + + *Derek Prior & Francesco Rodriguez* + +* Fixed support for DATABASE_URL environment variable for rake db tasks. *Grace Liu* + * rails dbconsole now can use SSL for MySQL. The database.yml options sslca, sslcert, sslcapath, sslcipher, and sslkey now affect rails dbconsole. *Jim Kingdon and Lars Petrus* diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 0202e86f32..7a0bb21043 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -41,7 +41,7 @@ module Rails @exceptions_app = nil @autoflush_log = true @log_formatter = ActiveSupport::Logger::SimpleFormatter.new - @queue = Rails::Queueing::Queue + @queue = Rails::Queueing::SynchronousQueue @queue_consumer = Rails::Queueing::ThreadedConsumer @eager_load = nil diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index d69afcd2cb..184c59cb90 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -16,53 +16,56 @@ module Rails attr_accessor :rails_template add_shebang_option! - argument :app_path, :type => :string + argument :app_path, type: :string def self.add_shared_options_for(name) - class_option :builder, :type => :string, :aliases => "-b", - :desc => "Path to a #{name} builder (can be a filesystem path or URL)" + class_option :builder, type: :string, aliases: '-b', + desc: "Path to a #{name} builder (can be a filesystem path or URL)" - class_option :template, :type => :string, :aliases => "-m", - :desc => "Path to an #{name} template (can be a filesystem path or URL)" + class_option :template, type: :string, aliases: '-m', + desc: "Path to an #{name} template (can be a filesystem path or URL)" - class_option :skip_gemfile, :type => :boolean, :default => false, - :desc => "Don't create a Gemfile" + class_option :skip_gemfile, type: :boolean, default: false, + desc: "Don't create a Gemfile" - class_option :skip_bundle, :type => :boolean, :default => false, - :desc => "Don't run bundle install" + class_option :skip_bundle, type: :boolean, default: false, + desc: "Don't run bundle install" - class_option :skip_git, :type => :boolean, :aliases => "-G", :default => false, - :desc => "Skip Git ignores and keeps" + class_option :skip_git, type: :boolean, aliases: '-G', default: false, + desc: 'Skip .gitignore file' - class_option :skip_active_record, :type => :boolean, :aliases => "-O", :default => false, - :desc => "Skip Active Record files" + class_option :skip_keeps, type: :boolean, default: false, + desc: 'Skip source control .keep files' - class_option :skip_sprockets, :type => :boolean, :aliases => "-S", :default => false, - :desc => "Skip Sprockets files" + class_option :skip_active_record, type: :boolean, aliases: '-O', default: false, + desc: 'Skip Active Record files' - class_option :database, :type => :string, :aliases => "-d", :default => "sqlite3", - :desc => "Preconfigure for selected database (options: #{DATABASES.join('/')})" + class_option :skip_sprockets, type: :boolean, aliases: '-S', default: false, + desc: 'Skip Sprockets files' - class_option :javascript, :type => :string, :aliases => '-j', :default => 'jquery', - :desc => 'Preconfigure for selected JavaScript library' + class_option :database, type: :string, aliases: '-d', default: 'sqlite3', + desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})" - class_option :skip_javascript, :type => :boolean, :aliases => "-J", :default => false, - :desc => "Skip JavaScript files" + class_option :javascript, type: :string, aliases: '-j', default: 'jquery', + desc: 'Preconfigure for selected JavaScript library' - class_option :skip_index_html, :type => :boolean, :aliases => "-I", :default => false, - :desc => "Skip public/index.html and app/assets/images/rails.png files" + class_option :skip_javascript, type: :boolean, aliases: '-J', default: false, + desc: 'Skip JavaScript files' - class_option :dev, :type => :boolean, :default => false, - :desc => "Setup the #{name} with Gemfile pointing to your Rails checkout" + class_option :skip_index_html, type: :boolean, aliases: '-I', default: false, + desc: 'Skip public/index.html and app/assets/images/rails.png files' - class_option :edge, :type => :boolean, :default => false, - :desc => "Setup the #{name} with Gemfile pointing to Rails repository" + class_option :dev, type: :boolean, default: false, + desc: "Setup the #{name} with Gemfile pointing to your Rails checkout" - class_option :skip_test_unit, :type => :boolean, :aliases => "-T", :default => false, - :desc => "Skip Test::Unit files" + class_option :edge, type: :boolean, default: false, + desc: "Setup the #{name} with Gemfile pointing to Rails repository" - class_option :help, :type => :boolean, :aliases => "-h", :group => :rails, - :desc => "Show this help message and quit" + class_option :skip_test_unit, type: :boolean, aliases: '-T', default: false, + desc: 'Skip Test::Unit files' + + class_option :help, type: :boolean, aliases: '-h', group: :rails, + desc: 'Show this help message and quit' end def initialize(*args) @@ -261,13 +264,13 @@ module Rails bundle_command('install') unless options[:skip_gemfile] || options[:skip_bundle] || options[:pretend] end - def empty_directory_with_gitkeep(destination, config = {}) + def empty_directory_with_keep_file(destination, config = {}) empty_directory(destination, config) - git_keep(destination) + keep_file(destination) end - def git_keep(destination) - create_file("#{destination}/.gitkeep") unless options[:skip_git] + def keep_file(destination) + create_file("#{destination}/.keep") unless options[:skip_keeps] end end end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index c06b0f8994..b71b16b043 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -11,7 +11,7 @@ module Rails private %w(template copy_file directory empty_directory inside - empty_directory_with_gitkeep create_file chmod shebang).each do |method| + empty_directory_with_keep_file create_file chmod shebang).each do |method| class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args, &block) @generator.send(:#{method}, *args, &block) @@ -55,8 +55,8 @@ module Rails def app directory 'app' - git_keep 'app/mailers' - git_keep 'app/models' + keep_file 'app/mailers' + keep_file 'app/models' end def config @@ -86,13 +86,13 @@ module Rails end def lib - empty_directory "lib" - empty_directory_with_gitkeep "lib/tasks" - empty_directory_with_gitkeep "lib/assets" + empty_directory 'lib' + empty_directory_with_keep_file 'lib/tasks' + empty_directory_with_keep_file 'lib/assets' end def log - empty_directory_with_gitkeep "log" + empty_directory_with_keep_file 'log' end def public_directory @@ -100,7 +100,7 @@ module Rails if options[:skip_index_html] remove_file "public/index.html" remove_file 'app/assets/images/rails.png' - git_keep 'app/assets/images' + keep_file 'app/assets/images' end end @@ -112,13 +112,13 @@ module Rails end def test - empty_directory_with_gitkeep "test/fixtures" - empty_directory_with_gitkeep "test/functional" - empty_directory_with_gitkeep "test/integration" - empty_directory_with_gitkeep "test/unit" + empty_directory_with_keep_file 'test/fixtures' + empty_directory_with_keep_file 'test/functional' + empty_directory_with_keep_file 'test/integration' + empty_directory_with_keep_file 'test/unit' - template "test/performance/browsing_test.rb" - template "test/test_helper.rb" + template 'test/performance/browsing_test.rb' + template 'test/test_helper.rb' end def tmp @@ -132,11 +132,11 @@ module Rails end def vendor_javascripts - empty_directory_with_gitkeep "vendor/assets/javascripts" + empty_directory_with_keep_file 'vendor/assets/javascripts' end def vendor_stylesheets - empty_directory_with_gitkeep "vendor/assets/stylesheets" + empty_directory_with_keep_file 'vendor/assets/stylesheets' end end 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 1ac0248bcf..09b08d5663 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -59,8 +59,5 @@ module <%= app_const_base %> # Version of your assets, change this if you want to expire all your assets. config.assets.version = '1.0' <% end -%> - - # Enable app-wide asynchronous ActionMailer. - # config.action_mailer.async = true end end 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 122e7e2b34..bcd0e7c898 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 @@ -41,7 +41,4 @@ # Debug mode disables concatenation and preprocessing of assets. config.assets.debug = true <%- end -%> - - # In development, use an in-memory queue for queueing. - config.queue = Rails::Queueing::Queue 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 a627636089..fdf011a510 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 @@ -80,7 +80,7 @@ # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new - # Default the production mode queue to an in-memory queue. You will probably + # Default the production mode queue to an synchronous queue. You will probably # want to replace this with an out-of-process queueing solution. - config.queue = Rails::Queueing::Queue + # config.queue = Rails::Queueing::SynchronousQueue end diff --git a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb index 4f937ad65a..c77b3450a3 100644 --- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb +++ b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb @@ -10,15 +10,15 @@ module Rails def app if mountable? - directory "app" - empty_directory_with_gitkeep "app/assets/images/#{name}" + directory 'app' + empty_directory_with_keep_file "app/assets/images/#{name}" elsif full? - empty_directory_with_gitkeep "app/models" - empty_directory_with_gitkeep "app/controllers" - empty_directory_with_gitkeep "app/views" - empty_directory_with_gitkeep "app/helpers" - empty_directory_with_gitkeep "app/mailers" - empty_directory_with_gitkeep "app/assets/images/#{name}" + empty_directory_with_keep_file 'app/models' + empty_directory_with_keep_file 'app/controllers' + empty_directory_with_keep_file 'app/views' + empty_directory_with_keep_file 'app/helpers' + empty_directory_with_keep_file 'app/mailers' + empty_directory_with_keep_file "app/assets/images/#{name}" end end @@ -110,7 +110,7 @@ task :default => :test copy_file "#{app_templates_dir}/app/assets/stylesheets/application.css", "app/assets/stylesheets/#{name}/application.css" elsif full? - empty_directory_with_gitkeep "app/assets/stylesheets/#{name}" + empty_directory_with_keep_file "app/assets/stylesheets/#{name}" end end @@ -121,7 +121,7 @@ task :default => :test template "#{app_templates_dir}/app/assets/javascripts/application.js.tt", "app/assets/javascripts/#{name}/application.js" elsif full? - empty_directory_with_gitkeep "app/assets/javascripts/#{name}" + empty_directory_with_keep_file "app/assets/javascripts/#{name}" end end diff --git a/railties/lib/rails/queueing.rb b/railties/lib/rails/queueing.rb index baf6811d3e..7cd755b0f7 100644 --- a/railties/lib/rails/queueing.rb +++ b/railties/lib/rails/queueing.rb @@ -35,6 +35,14 @@ module Rails class Queue < ::Queue end + class SynchronousQueue < ::Queue + def push(job) + job.run + end + alias << push + alias enq push + end + # In test mode, the Rails queue is backed by an Array so that assertions # can be made about its contents. The test queue provides a +jobs+ # method to make assertions about the queue's contents and a +drain+ diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index d3bbac811c..1eb5fce384 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -50,6 +50,23 @@ module ApplicationTests assert_equal "test.rails", ActionMailer::Base.default_url_options[:host] end + test "uses the default queue for ActionMailer" do + require "#{app_path}/config/environment" + assert_kind_of Rails::Queueing::Container, ActionMailer::Base.queue + end + + test "allows me to configure queue for ActionMailer" do + app_file "config/environments/development.rb", <<-RUBY + AppTemplate::Application.configure do + Rails.queue[:mailer] = Rails::Queueing::TestQueue.new + config.action_mailer.queue = Rails.queue[:mailer] + end + RUBY + + require "#{app_path}/config/environment" + assert_kind_of Rails::Queueing::TestQueue, ActionMailer::Base.queue + end + test "does not include url helpers as action methods" do app_file "config/routes.rb", <<-RUBY AppTemplate::Application.routes.draw do @@ -195,5 +212,37 @@ module ApplicationTests assert !ActiveRecord::Base.connection.schema_cache.tables["posts"] } end + + test "active record establish_connection uses Rails.env if DATABASE_URL is not set" do + begin + require "#{app_path}/config/environment" + orig_database_url = ENV.delete("DATABASE_URL") + orig_rails_env, Rails.env = Rails.env, 'development' + ActiveRecord::Base.establish_connection + assert ActiveRecord::Base.connection + assert_match /#{ActiveRecord::Base.configurations[Rails.env]['database']}/, ActiveRecord::Base.connection_config[:database] + ensure + ActiveRecord::Base.remove_connection + ENV["DATABASE_URL"] = orig_database_url if orig_database_url + Rails.env = orig_rails_env if orig_rails_env + end + end + + test "active record establish_connection uses DATABASE_URL even if Rails.env is set" do + begin + require "#{app_path}/config/environment" + orig_database_url = ENV.delete("DATABASE_URL") + orig_rails_env, Rails.env = Rails.env, 'development' + database_url_db_name = "db/database_url_db.sqlite3" + ENV["DATABASE_URL"] = "sqlite3://:@localhost/#{database_url_db_name}" + ActiveRecord::Base.establish_connection + assert ActiveRecord::Base.connection + assert_match /#{database_url_db_name}/, ActiveRecord::Base.connection_config[:database] + ensure + ActiveRecord::Base.remove_connection + ENV["DATABASE_URL"] = orig_database_url if orig_database_url + Rails.env = orig_rails_env if orig_rails_env + end + end end end diff --git a/railties/test/application/queue_test.rb b/railties/test/application/queue_test.rb index c856089540..f4c11c5f4f 100644 --- a/railties/test/application/queue_test.rb +++ b/railties/test/application/queue_test.rb @@ -23,10 +23,10 @@ module ApplicationTests assert_kind_of Rails::Queueing::TestQueue, Rails.queue[:default] end - test "the queue is a Queue in development mode" do + test "the queue is a SynchronousQueue in development mode" do app("development") - assert_kind_of Rails::Queueing::Queue, Rails.application.queue[:default] - assert_kind_of Rails::Queueing::Queue, Rails.queue[:default] + assert_kind_of Rails::Queueing::SynchronousQueue, Rails.application.queue[:default] + assert_kind_of Rails::Queueing::SynchronousQueue, Rails.queue[:default] end class ThreadTrackingJob @@ -47,7 +47,7 @@ module ApplicationTests end end - test "in development mode, an enqueued job will be processed in a separate thread" do + test "in development mode, an enqueued job will be processed in the same thread" do app("development") job = ThreadTrackingJob.new @@ -55,7 +55,7 @@ module ApplicationTests sleep 0.1 assert job.ran?, "Expected job to be run" - assert job.ran_in_different_thread?, "Expected job to run in a different thread" + refute job.ran_in_different_thread?, "Expected job to run in the same thread" end test "in test mode, explicitly draining the queue will process it in a separate thread" do @@ -160,6 +160,7 @@ module ApplicationTests test "a custom consumer implementation can be provided" do add_to_env_config "production", <<-RUBY require "my_queue_consumer" + config.queue = Rails::Queueing::Queue config.queue_consumer = MyQueueConsumer RUBY diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb new file mode 100644 index 0000000000..52c07cee9f --- /dev/null +++ b/railties/test/application/rake/dbs_test.rb @@ -0,0 +1,181 @@ +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeDbsTest < ActiveSupport::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 database_url_db_name + "db/database_url_db.sqlite3" + end + + def set_database_url + ENV['DATABASE_URL'] = "sqlite3://:@localhost/#{database_url_db_name}" + end + + def expected + @expected ||= {} + end + + def db_create_and_drop + Dir.chdir(app_path) do + output = `bundle exec rake db:create` + assert_equal output, "" + assert File.exists?(expected[:database]) + assert_equal expected[:database], + ActiveRecord::Base.connection_config[:database] + output = `bundle exec rake db:drop` + assert_equal output, "" + assert !File.exists?(expected[:database]) + end + end + + test 'db:create and db:drop without database url' do + require "#{app_path}/config/environment" + expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database'] + db_create_and_drop + end + + test 'db:create and db:drop with database url' do + require "#{app_path}/config/environment" + set_database_url + expected[:database] = database_url_db_name + db_create_and_drop + end + + def db_migrate_and_status + Dir.chdir(app_path) do + `rails generate model book title:string` + `bundle exec rake db:migrate` + output = `bundle exec rake db:migrate:status` + assert_match(/database:\s+\S+#{expected[:database]}/, output) + assert_match(/up\s+\d{14}\s+Create books/, output) + end + end + + test 'db:migrate and db:migrate:status without database_url' do + require "#{app_path}/config/environment" + expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database'] + db_migrate_and_status + end + + test 'db:migrate and db:migrate:status with database_url' do + require "#{app_path}/config/environment" + set_database_url + expected[:database] = database_url_db_name + db_migrate_and_status + end + + def db_schema_dump + Dir.chdir(app_path) do + `rails generate model book title:string` + `rake db:migrate` + `rake db:schema:dump` + schema_dump = File.read("db/schema.rb") + assert_match(/create_table \"books\"/, schema_dump) + end + end + + test 'db:schema:dump without database_url' do + db_schema_dump + end + + test 'db:schema:dump with database_url' do + set_database_url + db_schema_dump + end + + def db_fixtures_load + Dir.chdir(app_path) do + `rails generate model book title:string` + `bundle exec rake db:migrate` + `bundle exec rake db:fixtures:load` + assert_match /#{expected[:database]}/, + ActiveRecord::Base.connection_config[:database] + require "#{app_path}/app/models/book" + assert_equal 2, Book.count + end + end + + test 'db:fixtures:load without database_url' do + require "#{app_path}/config/environment" + expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database'] + db_fixtures_load + end + + test 'db:fixtures:load with database_url' do + require "#{app_path}/config/environment" + set_database_url + expected[:database] = database_url_db_name + db_fixtures_load + end + + def db_structure_dump_and_load + Dir.chdir(app_path) do + `rails generate model book title:string` + `bundle exec rake db:migrate` + `bundle exec rake db:structure:dump` + structure_dump = File.read("db/structure.sql") + assert_match(/CREATE TABLE \"books\"/, structure_dump) + `bundle exec rake db:drop` + `bundle exec rake db:structure:load` + assert_match /#{expected[:database]}/, + ActiveRecord::Base.connection_config[:database] + require "#{app_path}/app/models/book" + #if structure is not loaded correctly, exception would be raised + assert Book.count, 0 + end + end + + test 'db:structure:dump and db:structure:load without database_url' do + require "#{app_path}/config/environment" + expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database'] + db_structure_dump_and_load + end + + test 'db:structure:dump and db:structure:load with database_url' do + require "#{app_path}/config/environment" + set_database_url + expected[:database] = database_url_db_name + db_structure_dump_and_load + end + + def db_test_load_structure + Dir.chdir(app_path) do + `rails generate model book title:string` + `bundle exec rake db:migrate` + `bundle exec rake db:structure:dump` + `bundle exec rake db:test:load_structure` + ActiveRecord::Base.configurations = Rails.application.config.database_configuration + ActiveRecord::Base.establish_connection 'test' + require "#{app_path}/app/models/book" + #if structure is not loaded correctly, exception would be raised + assert Book.count, 0 + assert_match /#{ActiveRecord::Base.configurations['test']['database']}/, + ActiveRecord::Base.connection_config[:database] + end + end + + test 'db:test:load_structure without database_url' do + require "#{app_path}/config/environment" + db_test_load_structure + end + + test 'db:test:load_structure with database_url' do + require "#{app_path}/config/environment" + set_database_url + db_test_load_structure + end + end + end +end
\ No newline at end of file diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index c294bfb238..3ceb8c22a0 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -250,10 +250,10 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_generator_if_skip_index_html_is_given - run_generator [destination_root, "--skip-index-html"] - assert_no_file "public/index.html" - assert_no_file "app/assets/images/rails.png" - assert_file "app/assets/images/.gitkeep" + run_generator [destination_root, '--skip-index-html'] + assert_no_file 'public/index.html' + assert_no_file 'app/assets/images/rails.png' + assert_file 'app/assets/images/.keep' end def test_creation_of_a_test_directory diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index e78e67725d..a4bdfcf438 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -127,6 +127,18 @@ module SharedGeneratorTests # generated. assert_file 'Gemfile' end + + def test_skip_git + run_generator [destination_root, '--skip-git', '--full'] + assert_no_file('.gitignore') + assert_file('app/mailers/.keep') + end + + def test_skip_keeps + run_generator [destination_root, '--skip-keeps', '--full'] + assert_file('.gitignore') + assert_no_file('app/mailers/.keep') + end end module SharedCustomGeneratorTests |