diff options
56 files changed, 728 insertions, 750 deletions
diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb index c95ef8a0c7..27871ef351 100644 --- a/actionpack/test/controller/localized_templates_test.rb +++ b/actionpack/test/controller/localized_templates_test.rb @@ -8,22 +8,24 @@ end class LocalizedTemplatesTest < ActionController::TestCase tests LocalizedController + setup do + @old_locale = I18n.locale + end + + teardown do + I18n.locale = @old_locale + end + def test_localized_template_is_used - old_locale = I18n.locale I18n.locale = :de get :hello_world assert_equal "Gutten Tag", @response.body - ensure - I18n.locale = old_locale end def test_default_locale_template_is_used_when_locale_is_missing - old_locale = I18n.locale I18n.locale = :dk get :hello_world assert_equal "Hello World", @response.body - ensure - I18n.locale = old_locale end def test_use_fallback_locales @@ -36,13 +38,9 @@ class LocalizedTemplatesTest < ActionController::TestCase end def test_localized_template_has_correct_header_with_no_format_in_template_name - old_locale = I18n.locale I18n.locale = :it - get :hello_world assert_equal "Ciao Mondo", @response.body assert_equal "text/html", @response.content_type - ensure - I18n.locale = old_locale end end diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb index b5e74e373d..af50e11261 100644 --- a/actionpack/test/controller/render_other_test.rb +++ b/actionpack/test/controller/render_other_test.rb @@ -1,9 +1,5 @@ require 'abstract_unit' -ActionController.add_renderer :simon do |says, options| - self.content_type = Mime::TEXT - self.response_body = "Simon says: #{says}" -end class RenderOtherTest < ActionController::TestCase class TestController < ActionController::Base @@ -15,7 +11,14 @@ class RenderOtherTest < ActionController::TestCase tests TestController def test_using_custom_render_option + ActionController.add_renderer :simon do |says, options| + self.content_type = Mime::TEXT + self.response_body = "Simon says: #{says}" + end + get :render_simon_says assert_equal "Simon says: foo", @response.body + ensure + ActionController.remove_renderer :simon end end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 07c2115832..2a5aad9c0e 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -127,11 +127,12 @@ module RequestForgeryProtectionTests @token = "cf50faa3fe97702ca1ae" SecureRandom.stubs(:base64).returns(@token) + @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token ActionController::Base.request_forgery_protection_token = :custom_authenticity_token end def teardown - ActionController::Base.request_forgery_protection_token = nil + ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token end def test_should_render_form_with_token_tag @@ -376,11 +377,12 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController include RequestForgeryProtectionTests setup do + @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token ActionController::Base.request_forgery_protection_token = :custom_authenticity_token end teardown do - ActionController::Base.request_forgery_protection_token = nil + ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token end test 'should emit a csrf-param meta tag and a csrf-token meta tag' do @@ -465,11 +467,12 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase @old_logger = ActionController::Base.logger @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new @token = "foobar" + @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token ActionController::Base.request_forgery_protection_token = @token end def teardown - ActionController::Base.request_forgery_protection_token = nil + ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token super end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index aee139b95e..2c833aa2e5 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -32,14 +32,18 @@ end class SendFileTest < ActionController::TestCase include TestFileUtils - Mime::Type.register "image/png", :png unless defined? Mime::PNG - def setup + @mime_type_defined = defined? Mime::PNG + Mime::Type.register "image/png", :png unless @mime_type_defined @controller = SendFileController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end + teardown do + Mime::Type.unregister :png unless @mime_type_defined + end + def test_file_nostream @controller.options = { :stream => false } response = nil diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index a427113763..c9777ae71f 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3478,6 +3478,33 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest @app.draw(&block) end + def test_missing_controller + ex = assert_raises(ArgumentError) { + draw do + get '/foo/bar', :to => :index + end + } + assert_match(/Missing :controller/, ex.message) + end + + def test_missing_action + ex = assert_raises(ArgumentError) { + draw do + get '/foo/bar', :to => 'foo' + end + } + assert_match(/Missing :action/, ex.message) + end + + def test_missing_action_on_hash + ex = assert_raises(ArgumentError) { + draw do + get '/foo/bar', :to => 'foo#' + end + } + assert_match(/Missing :action/, ex.message) + end + def test_valid_controller_options_inside_namespace draw do namespace :admin do diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index e5cb843670..049af275b6 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -48,7 +48,7 @@ module ActionView # Change allowed default attributes # # class Application < Rails::Application - # config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style' + # config.action_view.sanitized_allowed_attributes = ['id', 'class', 'style'] # end # # Please note that sanitizing user-provided text does not guarantee that the @@ -204,7 +204,7 @@ module ActionView # Adds to the Set of allowed HTML attributes for the +sanitize+ helper. # # class Application < Rails::Application - # config.action_view.sanitized_allowed_attributes = 'onclick', 'longdesc' + # config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc'] # end # def sanitized_allowed_attributes=(attributes) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index bc47412405..7ff5001796 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -227,6 +227,10 @@ module ActiveRecord end end + def open_transactions + @transaction.number + end + def current_transaction #:nodoc: @transaction end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 6ecd4efdc8..a8e0cb93a0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,9 +1,9 @@ require 'date' require 'bigdecimal' require 'bigdecimal/util' +require 'active_record/type' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' -require 'active_record/connection_adapters/type' require 'active_record/connection_adapters/abstract/schema_dumper' require 'active_record/connection_adapters/abstract/schema_creation' require 'monitor' @@ -325,10 +325,6 @@ module ActiveRecord @connection end - def open_transactions - @transaction.number - end - def create_savepoint(name = nil) end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 86232f9d3f..a62617ab47 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -14,12 +14,9 @@ module ActiveRecord end attr_reader :name, :default, :cast_type, :null, :sql_type, :default_function - attr_accessor :coder - - alias :encoded? :coder delegate :type, :precision, :scale, :limit, :klass, :text?, :number?, :binary?, - :type_cast_for_write, :type_cast_for_database, to: :cast_type + :type_cast, :type_cast_for_write, :type_cast_for_database, to: :cast_type # Instantiates a new column in the table. # @@ -37,22 +34,12 @@ module ActiveRecord @null = null @default = extract_default(default) @default_function = nil - @coder = nil end def has_default? !default.nil? end - # Casts value to an appropriate instance. - def type_cast(value) - if encoded? - coder.load(value) - else - cast_type.type_cast(value) - end - end - # Returns the human name of the column name. # # ===== Examples diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index aa8a91ed39..909bba8c7d 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -298,7 +298,7 @@ module ActiveRecord end class << self - TYPES = ConnectionAdapters::Type::HashLookupTypeMap.new # :nodoc: + TYPES = Type::HashLookupTypeMap.new # :nodoc: delegate :register_type, :alias_type, to: :TYPES diff --git a/activerecord/lib/active_record/connection_adapters/type.rb b/activerecord/lib/active_record/connection_adapters/type.rb deleted file mode 100644 index bab7a3ff7e..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'active_record/connection_adapters/type/numeric' -require 'active_record/connection_adapters/type/time_value' -require 'active_record/connection_adapters/type/value' - -require 'active_record/connection_adapters/type/binary' -require 'active_record/connection_adapters/type/boolean' -require 'active_record/connection_adapters/type/date' -require 'active_record/connection_adapters/type/date_time' -require 'active_record/connection_adapters/type/decimal' -require 'active_record/connection_adapters/type/decimal_without_scale' -require 'active_record/connection_adapters/type/float' -require 'active_record/connection_adapters/type/integer' -require 'active_record/connection_adapters/type/string' -require 'active_record/connection_adapters/type/text' -require 'active_record/connection_adapters/type/time' - -require 'active_record/connection_adapters/type/type_map' -require 'active_record/connection_adapters/type/hash_lookup_type_map' - -module ActiveRecord - module ConnectionAdapters - module Type # :nodoc: - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/binary.rb b/activerecord/lib/active_record/connection_adapters/type/binary.rb deleted file mode 100644 index 4b2d1a66e0..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/binary.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class Binary < Value # :nodoc: - def type - :binary - end - - def binary? - true - end - - def klass - ::String - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/boolean.rb b/activerecord/lib/active_record/connection_adapters/type/boolean.rb deleted file mode 100644 index 2337bdd563..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/boolean.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class Boolean < Value # :nodoc: - def type - :boolean - end - - private - - def cast_value(value) - if value == '' - nil - else - Column::TRUE_VALUES.include?(value) - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/date.rb b/activerecord/lib/active_record/connection_adapters/type/date.rb deleted file mode 100644 index 1e7205fd0b..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/date.rb +++ /dev/null @@ -1,44 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class Date < Value # :nodoc: - def type - :date - end - - def klass - ::Date - end - - private - - def cast_value(value) - if value.is_a?(::String) - return if value.empty? - fast_string_to_date(value) || fallback_string_to_date(value) - elsif value.respond_to?(:to_date) - value.to_date - else - value - end - end - - def fast_string_to_date(string) - if string =~ Column::Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def new_date(year, mon, mday) - if year && year != 0 - ::Date.new(year, mon, mday) rescue nil - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/date_time.rb b/activerecord/lib/active_record/connection_adapters/type/date_time.rb deleted file mode 100644 index c34f4c5a53..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/date_time.rb +++ /dev/null @@ -1,35 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class DateTime < Value # :nodoc: - include TimeValue - - def type - :datetime - end - - private - - def cast_value(string) - return string unless string.is_a?(::String) - return if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 - end - - def fallback_string_to_time(string) - time_hash = ::Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/decimal.rb b/activerecord/lib/active_record/connection_adapters/type/decimal.rb deleted file mode 100644 index ac5af4b963..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/decimal.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class Decimal < Value # :nodoc: - include Numeric - - def type - :decimal - end - - def klass - ::BigDecimal - end - - private - - def cast_value(value) - if value.respond_to?(:to_d) - value.to_d - else - value.to_s.to_d - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/decimal_without_scale.rb b/activerecord/lib/active_record/connection_adapters/type/decimal_without_scale.rb deleted file mode 100644 index e58c6e198d..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/decimal_without_scale.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'active_record/connection_adapters/type/integer' - -module ActiveRecord - module ConnectionAdapters - module Type - class DecimalWithoutScale < Integer # :nodoc: - def type - :decimal - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/float.rb b/activerecord/lib/active_record/connection_adapters/type/float.rb deleted file mode 100644 index 28111e9d8e..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/float.rb +++ /dev/null @@ -1,25 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class Float < Value # :nodoc: - include Numeric - - def type - :float - end - - def klass - ::Float - end - - alias type_cast_for_database type_cast - - private - - def cast_value(value) - value.to_f - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb deleted file mode 100644 index bb1abc77ff..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class HashLookupTypeMap < TypeMap # :nodoc: - delegate :key?, to: :@mapping - - def lookup(type, *args) - @mapping.fetch(type, proc { default_value }).call(type, *args) - end - - def fetch(type, *args, &block) - @mapping.fetch(type, block).call(type, *args) - end - - def alias_type(type, alias_type) - register_type(type) { |_, *args| lookup(alias_type, *args) } - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/integer.rb b/activerecord/lib/active_record/connection_adapters/type/integer.rb deleted file mode 100644 index 8e6a509b5b..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/integer.rb +++ /dev/null @@ -1,29 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class Integer < Value # :nodoc: - include Numeric - - def type - :integer - end - - def klass - ::Fixnum - end - - alias type_cast_for_database type_cast - - private - - def cast_value(value) - case value - when true then 1 - when false then 0 - else value.to_i rescue nil - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/numeric.rb b/activerecord/lib/active_record/connection_adapters/type/numeric.rb deleted file mode 100644 index a3379831cb..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/numeric.rb +++ /dev/null @@ -1,20 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - module Numeric # :nodoc: - def number? - true - end - - def type_cast_for_write(value) - case value - when true then 1 - when false then 0 - when ::String then value.presence - else super - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/string.rb b/activerecord/lib/active_record/connection_adapters/type/string.rb deleted file mode 100644 index 55f0e1ee1c..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/string.rb +++ /dev/null @@ -1,29 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class String < Value # :nodoc: - def type - :string - end - - def text? - true - end - - def klass - ::String - end - - private - - def cast_value(value) - case value - when true then "1" - when false then "0" - else value.to_s - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/text.rb b/activerecord/lib/active_record/connection_adapters/type/text.rb deleted file mode 100644 index ee5842a3fc..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/text.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'active_record/connection_adapters/type/string' - -module ActiveRecord - module ConnectionAdapters - module Type - class Text < String # :nodoc: - def type - :text - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/time.rb b/activerecord/lib/active_record/connection_adapters/type/time.rb deleted file mode 100644 index bc331b0fa7..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/time.rb +++ /dev/null @@ -1,28 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class Time < Value - include TimeValue - - def type - :time - end - - private - - def cast_value(value) - return value unless value.is_a?(::String) - return if value.empty? - - dummy_time_value = "2000-01-01 #{value}" - - fast_string_to_time(dummy_time_value) || begin - time_hash = ::Date._parse(dummy_time_value) - return if time_hash[:hour].nil? - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/time_value.rb b/activerecord/lib/active_record/connection_adapters/type/time_value.rb deleted file mode 100644 index e9ca4adeda..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/time_value.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - module TimeValue # :nodoc: - def klass - ::Time - end - - private - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ Column::Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/type_map.rb b/activerecord/lib/active_record/connection_adapters/type/type_map.rb deleted file mode 100644 index 48b8b51417..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/type_map.rb +++ /dev/null @@ -1,50 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class TypeMap # :nodoc: - def initialize - @mapping = {} - end - - def lookup(lookup_key, *args) - matching_pair = @mapping.reverse_each.detect do |key, _| - key === lookup_key - end - - if matching_pair - matching_pair.last.call(lookup_key, *args) - else - default_value - end - end - - def register_type(key, value = nil, &block) - raise ::ArgumentError unless value || block - - if block - @mapping[key] = block - else - @mapping[key] = proc { value } - end - end - - def alias_type(key, target_key) - register_type(key) do |sql_type, *args| - metadata = sql_type[/\(.*\)/, 0] - lookup("#{target_key}#{metadata}", *args) - end - end - - def clear - @mapping.clear - end - - private - - def default_value - @default_value ||= Value.new - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/value.rb b/activerecord/lib/active_record/connection_adapters/type/value.rb deleted file mode 100644 index 415ef0aee9..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/value.rb +++ /dev/null @@ -1,61 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class Value # :nodoc: - attr_reader :precision, :scale, :limit - - # Valid options are +precision+, +scale+, and +limit+. - # They are only used when dumping schema. - def initialize(options = {}) - options.assert_valid_keys(:precision, :scale, :limit) - @precision = options[:precision] - @scale = options[:scale] - @limit = options[:limit] - end - - # The simplified that this object represents. Subclasses - # should override this method. - def type; end - - # Takes an input from the database, or from attribute setters, - # and casts it to a type appropriate for this object. This method - # should not be overriden by subclasses. Instead, override `cast_value`. - def type_cast(value) - cast_value(value) unless value.nil? - end - - def type_cast_for_write(value) - value - end - - def type_cast_for_database(value) - type_cast_for_write(value) - end - - def text? - false - end - - def number? - false - end - - def binary? - false - end - - def klass - ::Object - end - - private - - # Responsible for casting values from external sources to the appropriate - # type. Called by `type_cast` for all values except `nil`. - def cast_value(value) # :api: public - value - end - end - end - end -end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 71efbb8f93..2ccb1b0702 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -217,6 +217,13 @@ module ActiveRecord class ImmutableRelation < ActiveRecordError end + # TransactionIsolationError will be raised under the following conditions: + # + # * The adapter does not support setting the isolation level + # * You are joining an existing open transaction + # * You are creating a nested (savepoint) transaction + # + # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level. class TransactionIsolationError < ActiveRecordError end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 7dc7169a02..1242f49e28 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -516,7 +516,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - ConnectionAdapters::Type::Boolean.new.type_cast(hash['_destroy']) + Type::Boolean.new.type_cast(hash['_destroy']) end # Determines if a new record should be rejected by checking diff --git a/activerecord/lib/active_record/properties.rb b/activerecord/lib/active_record/properties.rb index a25c1cec58..c99594c823 100644 --- a/activerecord/lib/active_record/properties.rb +++ b/activerecord/lib/active_record/properties.rb @@ -2,7 +2,12 @@ module ActiveRecord module Properties # :nodoc: extend ActiveSupport::Concern - Type = ConnectionAdapters::Type + Type = ActiveRecord::Type + + included do + class_attribute :user_provided_columns, instance_accessor: false # :internal + self.user_provided_columns = {} + end module ClassMethods # Defines or overrides a property on this model. This allows customization of @@ -59,7 +64,8 @@ module ActiveRecord # store_listing.price_in_cents # => 1000 def property(name, cast_type) name = name.to_s - user_provided_columns[name] = ConnectionAdapters::Column.new(name, nil, cast_type) + # Assign a new hash to ensure that subclasses do not share a hash + self.user_provided_columns = user_provided_columns.merge(name => ConnectionAdapters::Column.new(name, nil, cast_type)) end # Returns an array of column objects for the table associated with this class. @@ -81,14 +87,15 @@ module ActiveRecord private - def user_provided_columns - @user_provided_columns ||= {} - end - def add_user_provided_columns(schema_columns) - schema_columns.reject { |column| - user_provided_columns.key? column.name - } + user_provided_columns.values + existing_columns = schema_columns.map do |column| + user_provided_columns[column.name] || column + end + + existing_column_names = existing_columns.map(&:name) + new_columns = user_provided_columns.except(*existing_column_names).values + + existing_columns + new_columns end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb new file mode 100644 index 0000000000..2c26477201 --- /dev/null +++ b/activerecord/lib/active_record/type.rb @@ -0,0 +1,18 @@ +require 'active_record/type/numeric' +require 'active_record/type/time_value' +require 'active_record/type/value' + +require 'active_record/type/binary' +require 'active_record/type/boolean' +require 'active_record/type/date' +require 'active_record/type/date_time' +require 'active_record/type/decimal' +require 'active_record/type/decimal_without_scale' +require 'active_record/type/float' +require 'active_record/type/integer' +require 'active_record/type/string' +require 'active_record/type/text' +require 'active_record/type/time' + +require 'active_record/type/type_map' +require 'active_record/type/hash_lookup_type_map' diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb new file mode 100644 index 0000000000..e34b7bb268 --- /dev/null +++ b/activerecord/lib/active_record/type/binary.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module Type + class Binary < Value # :nodoc: + def type + :binary + end + + def binary? + true + end + + def klass + ::String + end + end + end +end diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb new file mode 100644 index 0000000000..06dd17ed28 --- /dev/null +++ b/activerecord/lib/active_record/type/boolean.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Type + class Boolean < Value # :nodoc: + def type + :boolean + end + + private + + def cast_value(value) + if value == '' + nil + else + ConnectionAdapters::Column::TRUE_VALUES.include?(value) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb new file mode 100644 index 0000000000..45c69460ef --- /dev/null +++ b/activerecord/lib/active_record/type/date.rb @@ -0,0 +1,42 @@ +module ActiveRecord + module Type + class Date < Value # :nodoc: + def type + :date + end + + def klass + ::Date + end + + private + + def cast_value(value) + if value.is_a?(::String) + return if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end + end + + def fast_string_to_date(string) + if string =~ ConnectionAdapters::Column::Format::ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::Date.new(year, mon, mday) rescue nil + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb new file mode 100644 index 0000000000..560d63c101 --- /dev/null +++ b/activerecord/lib/active_record/type/date_time.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module Type + class DateTime < Value # :nodoc: + include TimeValue + + def type + :datetime + end + + private + + def cast_value(string) + return string unless string.is_a?(::String) + return if string.empty? + + fast_string_to_time(string) || fallback_string_to_time(string) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + time_hash = ::Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end + end + end +end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb new file mode 100644 index 0000000000..1c0147a797 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal.rb @@ -0,0 +1,25 @@ +module ActiveRecord + module Type + class Decimal < Value # :nodoc: + include Numeric + + def type + :decimal + end + + def klass + ::BigDecimal + end + + private + + def cast_value(value) + if value.respond_to?(:to_d) + value.to_d + else + value.to_s.to_d + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb new file mode 100644 index 0000000000..cabdcecdd7 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -0,0 +1,11 @@ +require 'active_record/type/integer' + +module ActiveRecord + module Type + class DecimalWithoutScale < Integer # :nodoc: + def type + :decimal + end + end + end +end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb new file mode 100644 index 0000000000..dc50dae328 --- /dev/null +++ b/activerecord/lib/active_record/type/float.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module Type + class Float < Value # :nodoc: + include Numeric + + def type + :float + end + + def klass + ::Float + end + + alias type_cast_for_database type_cast + + private + + def cast_value(value) + value.to_f + end + end + end +end diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb new file mode 100644 index 0000000000..bf92680268 --- /dev/null +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Type + class HashLookupTypeMap < TypeMap # :nodoc: + delegate :key?, to: :@mapping + + def lookup(type, *args) + @mapping.fetch(type, proc { default_value }).call(type, *args) + end + + def fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) + end + + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } + end + end + end +end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb new file mode 100644 index 0000000000..1e2147dec9 --- /dev/null +++ b/activerecord/lib/active_record/type/integer.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module Type + class Integer < Value # :nodoc: + include Numeric + + def type + :integer + end + + def klass + ::Fixnum + end + + alias type_cast_for_database type_cast + + private + + def cast_value(value) + case value + when true then 1 + when false then 0 + else value.to_i rescue nil + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb new file mode 100644 index 0000000000..464d631d80 --- /dev/null +++ b/activerecord/lib/active_record/type/numeric.rb @@ -0,0 +1,18 @@ +module ActiveRecord + module Type + module Numeric # :nodoc: + def number? + true + end + + def type_cast_for_write(value) + case value + when true then 1 + when false then 0 + when ::String then value.presence + else super + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb new file mode 100644 index 0000000000..b3f6ca7691 --- /dev/null +++ b/activerecord/lib/active_record/type/string.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module Type + class String < Value # :nodoc: + def type + :string + end + + def text? + true + end + + def klass + ::String + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + else value.to_s + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb new file mode 100644 index 0000000000..26f980f060 --- /dev/null +++ b/activerecord/lib/active_record/type/text.rb @@ -0,0 +1,11 @@ +require 'active_record/type/string' + +module ActiveRecord + module Type + class Text < String # :nodoc: + def type + :text + end + end + end +end diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb new file mode 100644 index 0000000000..41f7d97f0c --- /dev/null +++ b/activerecord/lib/active_record/type/time.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module Type + class Time < Value # :nodoc: + include TimeValue + + def type + :time + end + + private + + def cast_value(value) + return value unless value.is_a?(::String) + return if value.empty? + + dummy_time_value = "2000-01-01 #{value}" + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb new file mode 100644 index 0000000000..6cc19b6379 --- /dev/null +++ b/activerecord/lib/active_record/type/time_value.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module Type + module TimeValue # :nodoc: + def klass + ::Time + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb new file mode 100644 index 0000000000..88c5f9c497 --- /dev/null +++ b/activerecord/lib/active_record/type/type_map.rb @@ -0,0 +1,48 @@ +module ActiveRecord + module Type + class TypeMap # :nodoc: + def initialize + @mapping = {} + end + + def lookup(lookup_key, *args) + matching_pair = @mapping.reverse_each.detect do |key, _| + key === lookup_key + end + + if matching_pair + matching_pair.last.call(lookup_key, *args) + else + default_value + end + end + + def register_type(key, value = nil, &block) + raise ::ArgumentError unless value || block + + if block + @mapping[key] = block + else + @mapping[key] = proc { value } + end + end + + def alias_type(key, target_key) + register_type(key) do |sql_type, *args| + metadata = sql_type[/\(.*\)/, 0] + lookup("#{target_key}#{metadata}", *args) + end + end + + def clear + @mapping.clear + end + + private + + def default_value + @default_value ||= Value.new + end + end + end +end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb new file mode 100644 index 0000000000..72d27197d5 --- /dev/null +++ b/activerecord/lib/active_record/type/value.rb @@ -0,0 +1,59 @@ +module ActiveRecord + module Type + class Value # :nodoc: + attr_reader :precision, :scale, :limit + + # Valid options are +precision+, +scale+, and +limit+. + # They are only used when dumping schema. + def initialize(options = {}) + options.assert_valid_keys(:precision, :scale, :limit) + @precision = options[:precision] + @scale = options[:scale] + @limit = options[:limit] + end + + # The simplified that this object represents. Subclasses + # should override this method. + def type; end + + # Takes an input from the database, or from attribute setters, + # and casts it to a type appropriate for this object. This method + # should not be overriden by subclasses. Instead, override `cast_value`. + def type_cast(value) + cast_value(value) unless value.nil? + end + + def type_cast_for_write(value) + value + end + + def type_cast_for_database(value) + type_cast_for_write(value) + end + + def text? + false + end + + def number? + false + end + + def binary? + false + end + + def klass + ::Object + end + + private + + # Responsible for casting values from external sources to the appropriate + # type. Called by `type_cast` for all values except `nil`. + def cast_value(value) # :api: public + value + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 972abf7cdc..ecccbf10e6 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -83,7 +83,7 @@ end class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase include PostgresqlCompositeBehavior - class FullAddressType < ActiveRecord::ConnectionAdapters::Type::Value + class FullAddressType < ActiveRecord::Type::Value def type; :full_address end def type_cast(value) diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index 45e48900ee..bcfd66b4bf 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -11,26 +11,6 @@ module ActiveRecord @viz = @adapter.schema_creation end - def test_can_set_coder - column = Column.new("title", nil, Type::String.new, "varchar(20)") - column.coder = YAML - assert_equal YAML, column.coder - end - - def test_encoded? - column = Column.new("title", nil, Type::String.new, "varchar(20)") - assert !column.encoded? - - column.coder = YAML - assert column.encoded? - end - - def test_type_case_coded_column - column = Column.new("title", nil, Type::String.new, "varchar(20)") - column.coder = YAML - assert_equal "hello", column.type_cast("--- hello") - end - # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null diff --git a/activerecord/test/cases/connection_adapters/type/type_map_test.rb b/activerecord/test/cases/connection_adapters/type/type_map_test.rb deleted file mode 100644 index e85bbc8dc7..0000000000 --- a/activerecord/test/cases/connection_adapters/type/type_map_test.rb +++ /dev/null @@ -1,142 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module ConnectionAdapters - module Type - class TypeMapTest < ActiveRecord::TestCase - def test_default_type - mapping = TypeMap.new - - assert_kind_of Value, mapping.lookup(:undefined) - end - - def test_registering_types - boolean = Boolean.new - mapping = TypeMap.new - - mapping.register_type(/boolean/i, boolean) - - assert_equal mapping.lookup('boolean'), boolean - end - - def test_overriding_registered_types - time = Time.new - timestamp = DateTime.new - mapping = TypeMap.new - - mapping.register_type(/time/i, time) - mapping.register_type(/time/i, timestamp) - - assert_equal mapping.lookup('time'), timestamp - end - - def test_fuzzy_lookup - string = String.new - mapping = TypeMap.new - - mapping.register_type(/varchar/i, string) - - assert_equal mapping.lookup('varchar(20)'), string - end - - def test_aliasing_types - string = String.new - mapping = TypeMap.new - - mapping.register_type(/string/i, string) - mapping.alias_type(/varchar/i, 'string') - - assert_equal mapping.lookup('varchar'), string - end - - def test_changing_type_changes_aliases - time = Time.new - timestamp = DateTime.new - mapping = TypeMap.new - - mapping.register_type(/timestamp/i, time) - mapping.alias_type(/datetime/i, 'timestamp') - mapping.register_type(/timestamp/i, timestamp) - - assert_equal mapping.lookup('datetime'), timestamp - end - - def test_aliases_keep_metadata - mapping = TypeMap.new - - mapping.register_type(/decimal/i) { |sql_type| sql_type } - mapping.alias_type(/number/i, 'decimal') - - assert_equal mapping.lookup('number(20)'), 'decimal(20)' - assert_equal mapping.lookup('number'), 'decimal' - end - - def test_register_proc - string = String.new - binary = Binary.new - mapping = TypeMap.new - - mapping.register_type(/varchar/i) do |type| - if type.include?('(') - string - else - binary - end - end - - assert_equal mapping.lookup('varchar(20)'), string - assert_equal mapping.lookup('varchar'), binary - end - - def test_additional_lookup_args - mapping = TypeMap.new - - mapping.register_type(/varchar/i) do |type, limit| - if limit > 255 - 'text' - else - 'string' - end - end - mapping.alias_type(/string/i, 'varchar') - - assert_equal mapping.lookup('varchar', 200), 'string' - assert_equal mapping.lookup('varchar', 400), 'text' - assert_equal mapping.lookup('string', 400), 'text' - end - - def test_requires_value_or_block - mapping = TypeMap.new - - assert_raises(ArgumentError) do - mapping.register_type(/only key/i) - end - end - - def test_lookup_non_strings - mapping = HashLookupTypeMap.new - - mapping.register_type(1, 'string') - mapping.register_type(2, 'int') - mapping.alias_type(3, 1) - - assert_equal mapping.lookup(1), 'string' - assert_equal mapping.lookup(2), 'int' - assert_equal mapping.lookup(3), 'string' - assert_kind_of Type::Value, mapping.lookup(4) - end - - def test_clear_mappings - time = Time.new - mapping = TypeMap.new - - mapping.register_type(/time/i, time) - mapping.clear - - assert_not_equal mapping.lookup('time'), time - end - end - end - end -end - diff --git a/activerecord/test/cases/custom_properties_test.rb b/activerecord/test/cases/custom_properties_test.rb index 9598f0299c..397a8e0692 100644 --- a/activerecord/test/cases/custom_properties_test.rb +++ b/activerecord/test/cases/custom_properties_test.rb @@ -6,6 +6,13 @@ class OverloadedType < ActiveRecord::Base property :non_existent_decimal, Type::Decimal.new end +class ChildOfOverloadedType < OverloadedType +end + +class GrandchildOfOverloadedType < ChildOfOverloadedType + property :overloaded_float, Type::Float.new +end + class UnoverloadedType < ActiveRecord::Base self.table_name = 'overloaded_types' end @@ -60,5 +67,22 @@ module ActiveRecord assert_nil data.overloaded_float assert unoverloaded_data.overloaded_float end + + def test_children_inherit_custom_properties + data = ChildOfOverloadedType.new(overloaded_float: '4.4') + + assert_equal 4, data.overloaded_float + end + + def test_children_can_override_parents + data = GrandchildOfOverloadedType.new(overloaded_float: '4.4') + + assert_equal 4.4, data.overloaded_float + end + + def test_overloading_properties_does_not_change_column_order + column_names = OverloadedType.column_names + assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit non_existent_decimal), column_names + end end end diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb new file mode 100644 index 0000000000..4e32f92dd0 --- /dev/null +++ b/activerecord/test/cases/type/type_map_test.rb @@ -0,0 +1,130 @@ +require "cases/helper" + +module ActiveRecord + module Type + class TypeMapTest < ActiveRecord::TestCase + def test_default_type + mapping = TypeMap.new + + assert_kind_of Value, mapping.lookup(:undefined) + end + + def test_registering_types + boolean = Boolean.new + mapping = TypeMap.new + + mapping.register_type(/boolean/i, boolean) + + assert_equal mapping.lookup('boolean'), boolean + end + + def test_overriding_registered_types + time = Time.new + timestamp = DateTime.new + mapping = TypeMap.new + + mapping.register_type(/time/i, time) + mapping.register_type(/time/i, timestamp) + + assert_equal mapping.lookup('time'), timestamp + end + + def test_fuzzy_lookup + string = String.new + mapping = TypeMap.new + + mapping.register_type(/varchar/i, string) + + assert_equal mapping.lookup('varchar(20)'), string + end + + def test_aliasing_types + string = String.new + mapping = TypeMap.new + + mapping.register_type(/string/i, string) + mapping.alias_type(/varchar/i, 'string') + + assert_equal mapping.lookup('varchar'), string + end + + def test_changing_type_changes_aliases + time = Time.new + timestamp = DateTime.new + mapping = TypeMap.new + + mapping.register_type(/timestamp/i, time) + mapping.alias_type(/datetime/i, 'timestamp') + mapping.register_type(/timestamp/i, timestamp) + + assert_equal mapping.lookup('datetime'), timestamp + end + + def test_aliases_keep_metadata + mapping = TypeMap.new + + mapping.register_type(/decimal/i) { |sql_type| sql_type } + mapping.alias_type(/number/i, 'decimal') + + assert_equal mapping.lookup('number(20)'), 'decimal(20)' + assert_equal mapping.lookup('number'), 'decimal' + end + + def test_register_proc + string = String.new + binary = Binary.new + mapping = TypeMap.new + + mapping.register_type(/varchar/i) do |type| + if type.include?('(') + string + else + binary + end + end + + assert_equal mapping.lookup('varchar(20)'), string + assert_equal mapping.lookup('varchar'), binary + end + + def test_additional_lookup_args + mapping = TypeMap.new + + mapping.register_type(/varchar/i) do |type, limit| + if limit > 255 + 'text' + else + 'string' + end + end + mapping.alias_type(/string/i, 'varchar') + + assert_equal mapping.lookup('varchar', 200), 'string' + assert_equal mapping.lookup('varchar', 400), 'text' + assert_equal mapping.lookup('string', 400), 'text' + end + + def test_requires_value_or_block + mapping = TypeMap.new + + assert_raises(ArgumentError) do + mapping.register_type(/only key/i) + end + end + + def test_lookup_non_strings + mapping = HashLookupTypeMap.new + + mapping.register_type(1, 'string') + mapping.register_type(2, 'int') + mapping.alias_type(3, 1) + + assert_equal mapping.lookup(1), 'string' + assert_equal mapping.lookup(2), 'int' + assert_equal mapping.lookup(3), 'string' + assert_kind_of Type::Value, mapping.lookup(4) + end + end + end +end + diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index a160c462b2..e365435b50 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -162,7 +162,6 @@ - name: Upgrading Ruby on Rails url: upgrading_ruby_on_rails.html - work_in_progress: true description: This guide helps in upgrading applications to latest Ruby on Rails versions. - name: Ruby on Rails 4.1 Release Notes diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 027b6303fc..048eb9a6e3 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -17,7 +17,6 @@ After reading this guide, you will know: NOTE: This guide is not intended to be a complete documentation of available form helpers and their arguments. Please visit [the Rails API documentation](http://api.rubyonrails.org/) for a complete reference. - Dealing with Basic Forms ------------------------ @@ -32,18 +31,14 @@ The most basic form helper is `form_tag`. When called without arguments like this, it creates a `<form>` tag which, when submitted, will POST to the current page. For instance, assuming the current page is `/home/index`, the generated HTML will look like this (some line breaks added for readability): ```html -<form accept-charset="UTF-8" action="/home/index" method="post"> - <div style="margin:0;padding:0"> - <input name="utf8" type="hidden" value="✓" /> - <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" /> - </div> +<form accept-charset="UTF-8" action="/" method="post"> + <input name="utf8" type="hidden" value="✓" /> + <input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" /> Form contents </form> ``` -Now, you'll notice that the HTML contains something extra: a `div` element with two hidden input elements inside. This div is important, because the form cannot be successfully submitted without it. The first input element with name `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](./security.html#cross-site-request-forgery-csrf). - -NOTE: Throughout this guide, the `div` with the hidden input elements will be excluded from code samples for brevity. +You'll notice that the HTML contains `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element has name attribute of `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf). ### A Generic Search Form @@ -67,14 +62,15 @@ To create this form you will use `form_tag`, `label_tag`, `text_field_tag`, and This will generate the following HTML: ```html -<form accept-charset="UTF-8" action="/search" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="✓" /></div> +<form accept-charset="UTF-8" action="/search" method="get"> + <input name="utf8" type="hidden" value="✓" /> <label for="q">Search for:</label> <input id="q" name="q" type="text" /> <input name="commit" type="submit" value="Search" /> </form> ``` -TIP: For every form input, an ID attribute is generated from its name ("q" in the example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript. +TIP: For every form input, an ID attribute is generated from its name (`"q"` in above example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript. Besides `text_field_tag` and `submit_tag`, there is a similar helper for _every_ form control in HTML. @@ -146,7 +142,7 @@ Output: <label for="age_adult">I'm over 21</label> ``` -As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (`age`), the user will only be able to select one of them, and `params[:age]` will contain either "child" or "adult". +As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (`age`), the user will only be able to select one of them, and `params[:age]` will contain either `"child"` or `"adult"`. NOTE: Always use labels for checkbox and radio buttons. They associate text with a specific option and, by expanding the clickable region, @@ -217,7 +213,7 @@ Dealing with Model Objects ### Model Object Helpers -A particularly common task for a form is editing or creating a model object. While the `*_tag` helpers can certainly be used for this task they are somewhat verbose as for each tag you would have to ensure the correct parameter name is used and set the default value of the input appropriately. Rails provides helpers tailored to this task. These helpers lack the _tag suffix, for example `text_field`, `text_area`. +A particularly common task for a form is editing or creating a model object. While the `*_tag` helpers can certainly be used for this task they are somewhat verbose as for each tag you would have to ensure the correct parameter name is used and set the default value of the input appropriately. Rails provides helpers tailored to this task. These helpers lack the `_tag` suffix, for example `text_field`, `text_area`. For these helpers the first argument is the name of an instance variable and the second is the name of a method (usually an attribute) to call on that object. Rails will set the value of the input control to the return value of that method for the object and set an appropriate input name. If your controller has defined `@person` and that person's name is Henry then a form containing: @@ -239,7 +235,7 @@ Rails provides helpers for displaying the validation errors associated with a mo ### Binding a Form to an Object -While this is an increase in comfort it is far from perfect. If Person has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does. +While this is an increase in comfort it is far from perfect. If `Person` has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does. Assume we have a controller for dealing with articles `app/controllers/articles_controller.rb`: @@ -264,7 +260,7 @@ There are a few things to note here: * `@article` is the actual object being edited. * There is a single hash of options. Routing options are passed in the `:url` hash, HTML options are passed in the `:html` hash. Also you can provide a `:namespace` option for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id. * The `form_for` method yields a **form builder** object (the `f` variable). -* Methods to create form controls are called **on** the form builder object `f` +* Methods to create form controls are called **on** the form builder object `f`. The resulting HTML is: @@ -280,7 +276,7 @@ The name passed to `form_for` controls the key used in `params` to access the fo The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder. -You can create a similar binding without actually creating `<form>` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example if you had a Person model with an associated ContactDetail model you could create a form for creating both like so: +You can create a similar binding without actually creating `<form>` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example if you had a `Person` model with an associated `ContactDetail` model you could create a form for creating both like so: ```erb <%= form_for @person, url: {action: "create"} do |person_form| %> @@ -350,7 +346,6 @@ form_for [:admin, :management, @article] For more information on Rails' routing system and the associated conventions, please see the [routing guide](routing.html). - ### How do forms with PATCH, PUT, or DELETE methods work? The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PATCH" and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms. @@ -365,12 +360,11 @@ output: ```html <form accept-charset="UTF-8" action="/search" method="post"> - <div style="margin:0;padding:0"> - <input name="_method" type="hidden" value="patch" /> - <input name="utf8" type="hidden" value="✓" /> - <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" /> - </div> + <input name="_method" type="hidden" value="patch" /> + <input name="utf8" type="hidden" value="✓" /> + <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" /> ... +</form> ``` When parsing POSTed data, Rails will take into account the special `_method` parameter and acts as if the HTTP method was the one specified inside it ("PATCH" in this example). @@ -435,7 +429,7 @@ output: Whenever Rails sees that the internal value of an option being generated matches this value, it will add the `selected` attribute to that option. -TIP: The second argument to `options_for_select` must be exactly equal to the desired internal value. In particular if the value is the integer 2 you cannot pass "2" to `options_for_select` - you must pass 2. Be aware of values extracted from the `params` hash as they are all strings. +TIP: The second argument to `options_for_select` must be exactly equal to the desired internal value. In particular if the value is the integer `2` you cannot pass `"2"` to `options_for_select` - you must pass `2`. Be aware of values extracted from the `params` hash as they are all strings. WARNING: when `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one and `multiple` is not true. @@ -489,11 +483,11 @@ You can also pass a block to `select` helper: <% end %> ``` -WARNING: If you are using `select` (or similar helpers such as `collection_select`, `select_tag`) to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself. If you specify `city` instead of `city_id` Active Record will raise an error along the lines of ` ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750) ` when you pass the `params` hash to `Person.new` or `update`. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly. +WARNING: If you are using `select` (or similar helpers such as `collection_select`, `select_tag`) to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself. If you specify `city` instead of `city_id` Active Record will raise an error along the lines of `ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)` when you pass the `params` hash to `Person.new` or `update`. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly. ### Option Tags from a Collection of Arbitrary Objects -Generating options tags with `options_for_select` requires that you create an array containing the text and value for each option. But what if you had a City model (perhaps an Active Record one) and you wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them: +Generating options tags with `options_for_select` requires that you create an array containing the text and value for each option. But what if you had a `City` model (perhaps an Active Record one) and you wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them: ```erb <% cities_array = City.all.map { |city| [city.name, city.id] } %> @@ -540,7 +534,7 @@ Both of these families of helpers will create a series of select boxes for the d ### Barebones Helpers -The `select_*` family of helpers take as their first argument an instance of Date, Time or DateTime that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example +The `select_*` family of helpers take as their first argument an instance of `Date`, `Time` or `DateTime` that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example ```erb <%= select_date Date.today, prefix: :start_date %> @@ -554,7 +548,7 @@ outputs (with actual option values omitted for brevity) <select id="start_date_day" name="start_date[day]"> ... </select> ``` -The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual Time or Date object you would have to extract these values and pass them to the appropriate constructor, for example +The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual `Date`, `Time` or `DateTime` object you would have to extract these values and pass them to the appropriate constructor, for example ```ruby Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i) @@ -599,7 +593,7 @@ NOTE: In many cases the built-in date pickers are clumsy as they do not aid the Occasionally you need to display just a single date component such as a year or a month. Rails provides a series of helpers for this, one for each component `select_year`, `select_month`, `select_day`, `select_hour`, `select_minute`, `select_second`. These helpers are fairly straightforward. By default they will generate an input field named after the time component (for example "year" for `select_year`, "month" for `select_month` etc.) although this can be overridden with the `:field_name` option. The `:prefix` option works in the same way that it does for `select_date` and `select_time` and has the same default value. -The first parameter specifies which value should be selected and can either be an instance of a Date, Time or DateTime, in which case the relevant component will be extracted, or a numerical value. For example +The first parameter specifies which value should be selected and can either be an instance of a `Date`, `Time` or `DateTime`, in which case the relevant component will be extracted, or a numerical value. For example ```erb <%= select_year(2009) %> @@ -629,7 +623,7 @@ Rails provides the usual pair of helpers: the barebones `file_field_tag` and the ### What Gets Uploaded -The object in the `params` hash is an instance of a subclass of IO. Depending on the size of the uploaded file it may in fact be a StringIO or an instance of File backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example). +The object in the `params` hash is an instance of a subclass of `IO`. Depending on the size of the uploaded file it may in fact be a StringIO or an instance of `File` backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example). ```ruby def upload @@ -640,7 +634,7 @@ def upload end ``` -Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. The intricacies of this are beyond the scope of this guide, but there are several libraries designed to assist with these. Two of the better known ones are [CarrierWave](https://github.com/jnicklas/carrierwave) and [Paperclip](http://www.thoughtbot.com/projects/paperclip). +Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. The intricacies of this are beyond the scope of this guide, but there are several libraries designed to assist with these. Two of the better known ones are [CarrierWave](https://github.com/jnicklas/carrierwave) and [Paperclip](https://github.com/thoughtbot/paperclip). NOTE: If the user has not selected a file the corresponding parameter will be an empty string. @@ -651,7 +645,7 @@ Unlike other forms making an asynchronous file upload form is not as simple as p Customizing Form Builders ------------------------- -As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of FormBuilder (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass FormBuilder and add the helpers there. For example +As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of `FormBuilder` (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass `FormBuilder` and add the helpers there. For example ```erb <%= form_for @person do |f| %> @@ -667,7 +661,7 @@ can be replaced with <% end %> ``` -by defining a LabellingFormBuilder class similar to the following: +by defining a `LabellingFormBuilder` class similar to the following: ```ruby class LabellingFormBuilder < ActionView::Helpers::FormBuilder @@ -685,7 +679,7 @@ The form builder used also determines what happens when you do <%= render partial: f %> ``` -If `f` is an instance of FormBuilder then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class LabellingFormBuilder then the `labelling_form` partial would be rendered instead. +If `f` is an instance of `FormBuilder` then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class `LabellingFormBuilder` then the `labelling_form` partial would be rendered instead. Understanding Parameter Naming Conventions ------------------------------------------ @@ -862,7 +856,7 @@ Or if you don't want to render an `authenticity_token` field: Building Complex Forms ---------------------- -Many apps grow beyond simple forms editing a single object. For example when creating a Person you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary. +Many apps grow beyond simple forms editing a single object. For example when creating a `Person` you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary. ### Configuring the Model diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 1343f0a628..8a41f160fa 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -26,10 +26,6 @@ *Matthew Draper* -* Do not set the Rails environment to test by default when using test_unit Railtie. - - *Konstantin Shabanov* - * Remove sqlite3 lines from `.gitignore` if the application is not using sqlite3. *Dmitrii Golub* diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb index 878b9b7930..75180ff978 100644 --- a/railties/lib/rails/test_unit/railtie.rb +++ b/railties/lib/rails/test_unit/railtie.rb @@ -1,4 +1,4 @@ -if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^test(?::|$)/).any? +if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any? ENV['RAILS_ENV'] ||= 'test' end |