Merge branch 'master' into feature/reselect-method

This commit is contained in:
Willian Gustavo Veiga 2018-10-17 20:24:44 -03:00
commit c8ff9bd63a
49 changed files with 518 additions and 347 deletions

View File

@ -31,7 +31,7 @@ addons:
- postgresql-10 - postgresql-10
- postgresql-client-10 - postgresql-client-10
bundler_args: --without test --jobs 3 --retry 3 bundler_args: --jobs 3 --retry 3
before_install: before_install:
- "rm ${BUNDLE_GEMFILE}.lock" - "rm ${BUNDLE_GEMFILE}.lock"
- "travis_retry gem update --system" - "travis_retry gem update --system"

View File

@ -99,6 +99,7 @@ instance_eval File.read local_gemfile if File.exist? local_gemfile
group :test do group :test do
gem "minitest-bisect" gem "minitest-bisect"
gem "minitest-retry"
platforms :mri do platforms :mri do
gem "stackprof" gem "stackprof"

View File

@ -306,7 +306,7 @@ GEM
loofah (2.2.2) loofah (2.2.2)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.0) mail (2.7.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
marcel (0.3.3) marcel (0.3.3)
mimemagic (~> 0.3.2) mimemagic (~> 0.3.2)
@ -323,6 +323,8 @@ GEM
minitest-bisect (1.4.0) minitest-bisect (1.4.0)
minitest-server (~> 1.0) minitest-server (~> 1.0)
path_expander (~> 1.0) path_expander (~> 1.0)
minitest-retry (0.1.9)
minitest (>= 5.0)
minitest-server (1.0.5) minitest-server (1.0.5)
minitest (~> 5.0) minitest (~> 5.0)
mono_logger (1.1.0) mono_logger (1.1.0)
@ -538,6 +540,7 @@ DEPENDENCIES
libxml-ruby libxml-ruby
listen (>= 3.0.5, < 3.2) listen (>= 3.0.5, < 3.2)
minitest-bisect minitest-bisect
minitest-retry
mysql2 (>= 0.4.10) mysql2 (>= 0.4.10)
nokogiri (>= 1.8.1) nokogiri (>= 1.8.1)
pg (>= 0.18.0) pg (>= 0.18.0)
@ -576,4 +579,4 @@ DEPENDENCIES
websocket-client-simple! websocket-client-simple!
BUNDLED WITH BUNDLED WITH
1.16.5 1.16.6

View File

@ -152,9 +152,9 @@ class BaseTest < ActiveSupport::TestCase
assert_equal(2, email.parts.length) assert_equal(2, email.parts.length)
assert_equal("multipart/mixed", email.mime_type) assert_equal("multipart/mixed", email.mime_type)
assert_equal("text/html", email.parts[0].mime_type) assert_equal("text/html", email.parts[0].mime_type)
assert_equal("Attachment with content", email.parts[0].body.encoded) assert_equal("Attachment with content", email.parts[0].decoded)
assert_equal("application/pdf", email.parts[1].mime_type) assert_equal("application/pdf", email.parts[1].mime_type)
assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded) assert_equal("This is test File content", email.parts[1].decoded)
end end
test "adds the given :body as part" do test "adds the given :body as part" do
@ -162,9 +162,9 @@ class BaseTest < ActiveSupport::TestCase
assert_equal(2, email.parts.length) assert_equal(2, email.parts.length)
assert_equal("multipart/mixed", email.mime_type) assert_equal("multipart/mixed", email.mime_type)
assert_equal("text/plain", email.parts[0].mime_type) assert_equal("text/plain", email.parts[0].mime_type)
assert_equal("I'm the eggman", email.parts[0].body.encoded) assert_equal("I'm the eggman", email.parts[0].decoded)
assert_equal("application/pdf", email.parts[1].mime_type) assert_equal("application/pdf", email.parts[1].mime_type)
assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded) assert_equal("This is test File content", email.parts[1].decoded)
end end
test "can embed an inline attachment" do test "can embed an inline attachment" do

View File

@ -1,3 +1,7 @@
* Add `ActionController::Parameters#each_value`.
*Lukáš Zapletal*
* Deprecate `ActionDispatch::Http::ParameterFilter` in favor of `ActiveSupport::ParameterFilter`. * Deprecate `ActionDispatch::Http::ParameterFilter` in favor of `ActiveSupport::ParameterFilter`.
*Yoshiyuki Kinjo* *Yoshiyuki Kinjo*

View File

@ -352,7 +352,7 @@ module ActionController
# the same way as <tt>Hash#each_value</tt>. # the same way as <tt>Hash#each_value</tt>.
def each_value(&block) def each_value(&block)
@parameters.each_pair do |key, value| @parameters.each_pair do |key, value|
yield [convert_hashes_to_parameters(key, value)] yield convert_hashes_to_parameters(key, value)
end end
end end

View File

@ -77,11 +77,15 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
test "each_value carries permitted status" do test "each_value carries permitted status" do
@params.permit! @params.permit!
@params["person"].each_value { |value| assert(value.permitted?) if value == 32 } @params.each_value do |value|
assert_predicate(value, :permitted?)
end
end end
test "each_value carries unpermitted status" do test "each_value carries unpermitted status" do
@params["person"].each_value { |value| assert_not(value.permitted?) if value == 32 } @params.each_value do |value|
assert_not_predicate(value, :permitted?)
end
end end
test "each_key converts to hash for permitted" do test "each_key converts to hash for permitted" do

View File

@ -31,7 +31,7 @@ module ActiveJob
# jobs. Since jobs share a single thread pool, long-running jobs will block # jobs. Since jobs share a single thread pool, long-running jobs will block
# short-lived jobs. Fine for dev/test; bad for production. # short-lived jobs. Fine for dev/test; bad for production.
class AsyncAdapter class AsyncAdapter
# See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options. # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options.
def initialize(**executor_options) def initialize(**executor_options)
@scheduler = Scheduler.new(**executor_options) @scheduler = Scheduler.new(**executor_options)
end end

View File

@ -197,7 +197,7 @@ module ActiveJob
# assert_performed_jobs 2 # assert_performed_jobs 2
# end # end
# #
# If a block is passed, that block should cause the specified number of # If a block is passed, asserts that the block will cause the specified number of
# jobs to be performed. # jobs to be performed.
# #
# def test_jobs_again # def test_jobs_again
@ -279,7 +279,7 @@ module ActiveJob
# end # end
# end # end
# #
# If a block is passed, that block should not cause any job to be performed. # If a block is passed, asserts that the block will not cause any job to be performed.
# #
# def test_jobs_again # def test_jobs_again
# assert_no_performed_jobs do # assert_no_performed_jobs do
@ -347,7 +347,7 @@ module ActiveJob
# end # end
# #
# #
# If a block is passed, that block should cause the job to be # If a block is passed, asserts that the block will cause the job to be
# enqueued with the given arguments. # enqueued with the given arguments.
# #
# def test_assert_enqueued_with # def test_assert_enqueued_with

View File

@ -474,5 +474,43 @@ module ActiveModel
def _read_attribute(attr) def _read_attribute(attr)
__send__(attr) __send__(attr)
end end
module AttrNames # :nodoc:
DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
# We want to generate the methods via module_eval rather than
# define_method, because define_method is slower on dispatch.
# Evaluating many similar methods may use more memory as the instruction
# sequences are duplicated and cached (in MRI). define_method may
# be slower on dispatch, but if you're careful about the closure
# created, then define_method will consume much less memory.
#
# But sometimes the database might return columns with
# characters that are not allowed in normal method names (like
# 'my_column(omg)'. So to work around this we first define with
# the __temp__ identifier, and then use alias method to rename
# it to what we want.
#
# We are also defining a constant to hold the frozen string of
# the attribute name. Using a constant means that we do not have
# to allocate an object on each call to the attribute method.
# Making it frozen means that it doesn't get duped when used to
# key the @attributes in read_attribute.
def self.define_attribute_accessor_method(mod, attr_name, writer: false)
method_name = "#{attr_name}#{'=' if writer}"
if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
yield method_name, "'#{attr_name}'.freeze"
else
safe_name = attr_name.unpack1("h*")
const_name = "ATTR_#{safe_name}"
const_set(const_name, attr_name) unless const_defined?(const_name)
temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
yield temp_method_name, attr_name_expr
mod.send(:alias_method, method_name, temp_method_name)
mod.send(:undef_method, temp_method_name)
end
end
end
end end
end end

View File

@ -29,17 +29,16 @@ module ActiveModel
private private
def define_method_attribute=(name) def define_method_attribute=(name)
safe_name = name.unpack1("h*") ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name generated_attribute_methods, name, writer: true,
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def __temp__#{safe_name}=(value) def #{temp_method_name}(value)
name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name} name = #{attr_name_expr}
write_attribute(name, value) write_attribute(name, value)
end end
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= RUBY
undef_method :__temp__#{safe_name}= end
STR
end end
NO_DEFAULT_PROVIDED = Object.new # :nodoc: NO_DEFAULT_PROVIDED = Object.new # :nodoc:
@ -97,15 +96,4 @@ module ActiveModel
write_attribute(attribute_name, value) write_attribute(attribute_name, value)
end end
end end
module AttributeMethods #:nodoc:
AttrNames = Module.new {
def self.set_name_cache(name, value)
const_name = "ATTR_#{name}"
unless const_defined? const_name
const_set const_name, -value
end
end
}
end
end end

View File

@ -1,4 +1,12 @@
* Add `reselect` method. This is short-hand for `unscope(:select).select(fields)`. * Fix collection cache key with limit and custom select to avoid ambiguous timestamp column error.
Fixes #33056.
*Federico Martinez*
* Add `reselect` method. This is a short-hand for `unscope(:select).select(fields)`.
Fixes #27340.
*Willian Gustavo Veiga* *Willian Gustavo Veiga*

View File

@ -22,15 +22,6 @@ module ActiveRecord
delegate :column_for_attribute, to: :class delegate :column_for_attribute, to: :class
end end
AttrNames = Module.new {
def self.set_name_cache(name, value)
const_name = "ATTR_#{name}"
unless const_defined? const_name
const_set const_name, -value
end
end
}
RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
class GeneratedAttributeMethods < Module #:nodoc: class GeneratedAttributeMethods < Module #:nodoc:
@ -270,21 +261,14 @@ module ActiveRecord
def respond_to?(name, include_private = false) def respond_to?(name, include_private = false)
return false unless super return false unless super
case name
when :to_partial_path
name = "to_partial_path"
when :to_model
name = "to_model"
else
name = name.to_s
end
# If the result is true then check for the select case. # If the result is true then check for the select case.
# For queries selecting a subset of columns, return false for unselected columns. # For queries selecting a subset of columns, return false for unselected columns.
# We check defined?(@attributes) not to issue warnings if called on objects that # We check defined?(@attributes) not to issue warnings if called on objects that
# have been allocated but not yet initialized. # have been allocated but not yet initialized.
if defined?(@attributes) && self.class.column_names.include?(name) if defined?(@attributes)
return has_attribute?(name) if name = self.class.symbol_column_to_string(name.to_sym)
return has_attribute?(name)
end
end end
true true

View File

@ -8,42 +8,19 @@ module ActiveRecord
module ClassMethods # :nodoc: module ClassMethods # :nodoc:
private private
# We want to generate the methods via module_eval rather than
# define_method, because define_method is slower on dispatch.
# Evaluating many similar methods may use more memory as the instruction
# sequences are duplicated and cached (in MRI). define_method may
# be slower on dispatch, but if you're careful about the closure
# created, then define_method will consume much less memory.
#
# But sometimes the database might return columns with
# characters that are not allowed in normal method names (like
# 'my_column(omg)'. So to work around this we first define with
# the __temp__ identifier, and then use alias method to rename
# it to what we want.
#
# We are also defining a constant to hold the frozen string of
# the attribute name. Using a constant means that we do not have
# to allocate an object on each call to the attribute method.
# Making it frozen means that it doesn't get duped when used to
# key the @attributes in read_attribute.
def define_method_attribute(name) def define_method_attribute(name)
safe_name = name.unpack1("h*")
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
def #{temp_method} generated_attribute_methods, name
#{sync_with_transaction_state} ) do |temp_method_name, attr_name_expr|
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
_read_attribute(name) { |n| missing_attribute(n, caller) } def #{temp_method_name}
end #{sync_with_transaction_state}
STR name = #{attr_name_expr}
_read_attribute(name) { |n| missing_attribute(n, caller) }
generated_attribute_methods.module_eval do end
alias_method name, temp_method RUBY
undef_method temp_method
end end
end end
end end

View File

@ -13,19 +13,19 @@ module ActiveRecord
private private
def define_method_attribute=(name) def define_method_attribute=(name)
safe_name = name.unpack1("h*")
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
def __temp__#{safe_name}=(value) generated_attribute_methods, name, writer: true,
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} ) do |temp_method_name, attr_name_expr|
#{sync_with_transaction_state} generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
_write_attribute(name, value) def #{temp_method_name}(value)
end name = #{attr_name_expr}
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= #{sync_with_transaction_state}
undef_method :__temp__#{safe_name}= _write_attribute(name, value)
STR end
RUBY
end
end end
end end

View File

@ -20,9 +20,9 @@ module ActiveRecord
select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
if collection.has_limit_or_offset? if collection.has_limit_or_offset?
query = collection.select(column) query = collection.select("#{column} AS collection_cache_key_timestamp")
subquery_alias = "subquery_for_cache_key" subquery_alias = "subquery_for_cache_key"
subquery_column = "#{subquery_alias}.#{timestamp_column}" subquery_column = "#{subquery_alias}.collection_cache_key_timestamp"
subquery = query.arel.as(subquery_alias) subquery = query.arel.as(subquery_alias)
arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column) arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column)
else else

View File

@ -348,8 +348,8 @@ module ActiveRecord
# #
# create_table :taggings do |t| # create_table :taggings do |t|
# t.references :tag, index: { name: 'index_taggings_on_tag_id' } # t.references :tag, index: { name: 'index_taggings_on_tag_id' }
# t.references :tagger, polymorphic: true, index: true # t.references :tagger, polymorphic: true
# t.references :taggable, polymorphic: { default: 'Photo' } # t.references :taggable, polymorphic: { default: 'Photo' }, index: false
# end # end
def column(name, type, options = {}) def column(name, type, options = {})
name = name.to_s name = name.to_s

View File

@ -846,17 +846,17 @@ module ActiveRecord
# [<tt>:null</tt>] # [<tt>:null</tt>]
# Whether the column allows nulls. Defaults to true. # Whether the column allows nulls. Defaults to true.
# #
# ====== Create a user_id bigint column # ====== Create a user_id bigint column without a index
# #
# add_reference(:products, :user) # add_reference(:products, :user, index: false)
# #
# ====== Create a user_id string column # ====== Create a user_id string column
# #
# add_reference(:products, :user, type: :string) # add_reference(:products, :user, type: :string)
# #
# ====== Create supplier_id, supplier_type columns and appropriate index # ====== Create supplier_id, supplier_type columns
# #
# add_reference(:products, :supplier, polymorphic: true, index: true) # add_reference(:products, :supplier, polymorphic: true)
# #
# ====== Create a supplier_id column with a unique index # ====== Create a supplier_id column with a unique index
# #
@ -884,7 +884,7 @@ module ActiveRecord
# #
# ====== Remove the reference # ====== Remove the reference
# #
# remove_reference(:products, :user, index: true) # remove_reference(:products, :user, index: false)
# #
# ====== Remove polymorphic reference # ====== Remove polymorphic reference
# #
@ -892,7 +892,7 @@ module ActiveRecord
# #
# ====== Remove the reference with a foreign key # ====== Remove the reference with a foreign key
# #
# remove_reference(:products, :user, index: true, foreign_key: true) # remove_reference(:products, :user, foreign_key: true)
# #
def remove_reference(table_name, ref_name, foreign_key: false, polymorphic: false, **options) def remove_reference(table_name, ref_name, foreign_key: false, polymorphic: false, **options)
if foreign_key if foreign_key

View File

@ -125,6 +125,8 @@ module ActiveRecord
@advisory_locks_enabled = self.class.type_cast_config_to_boolean( @advisory_locks_enabled = self.class.type_cast_config_to_boolean(
config.fetch(:advisory_locks, true) config.fetch(:advisory_locks, true)
) )
check_version
end end
def replica? def replica?
@ -502,6 +504,9 @@ module ActiveRecord
end end
private private
def check_version
end
def type_map def type_map
@type_map ||= Type::TypeMap.new.tap do |mapping| @type_map ||= Type::TypeMap.new.tap do |mapping|
initialize_type_map(mapping) initialize_type_map(mapping)

View File

@ -54,10 +54,6 @@ module ActiveRecord
super(connection, logger, config) super(connection, logger, config)
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
if version < "5.5.8"
raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
end
end end
def version #:nodoc: def version #:nodoc:
@ -535,6 +531,12 @@ module ActiveRecord
end end
private private
def check_version
if version < "5.5.8"
raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
end
end
def combine_multi_statements(total_sql) def combine_multi_statements(total_sql)
total_sql.each_with_object([]) do |sql, total_sql_chunks| total_sql.each_with_object([]) do |sql, total_sql_chunks|
previous_packet = total_sql_chunks.last previous_packet = total_sql_chunks.last

View File

@ -43,9 +43,14 @@ module ActiveRecord
valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl] valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
conn_params.slice!(*valid_conn_param_keys) conn_params.slice!(*valid_conn_param_keys)
# The postgres drivers don't allow the creation of an unconnected PG::Connection object, conn = PG.connect(conn_params)
# so just pass a nil connection object for the time being. ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config)
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config) rescue ::PG::Error => error
if error.message.include?("does not exist")
raise ActiveRecord::NoDatabaseError
else
raise
end
end end
end end
@ -220,15 +225,11 @@ module ActiveRecord
@local_tz = nil @local_tz = nil
@max_identifier_length = nil @max_identifier_length = nil
connect configure_connection
add_pg_encoders add_pg_encoders
@statements = StatementPool.new @connection, @statements = StatementPool.new @connection,
self.class.type_cast_config_to_integer(config[:statement_limit]) self.class.type_cast_config_to_integer(config[:statement_limit])
if postgresql_version < 90100
raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1."
end
add_pg_decoders add_pg_decoders
@type_map = Type::HashLookupTypeMap.new @type_map = Type::HashLookupTypeMap.new
@ -410,6 +411,12 @@ module ActiveRecord
end end
private private
def check_version
if postgresql_version < 90100
raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1."
end
end
# See https://www.postgresql.org/docs/current/static/errcodes-appendix.html # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001" VALUE_LIMIT_VIOLATION = "22001"
NUMERIC_VALUE_OUT_OF_RANGE = "22003" NUMERIC_VALUE_OUT_OF_RANGE = "22003"
@ -699,12 +706,6 @@ module ActiveRecord
def connect def connect
@connection = PG.connect(@connection_parameters) @connection = PG.connect(@connection_parameters)
configure_connection configure_connection
rescue ::PG::Error => error
if error.message.include?("does not exist")
raise ActiveRecord::NoDatabaseError
else
raise
end
end end
# Configures the encoding, verbosity, schema search path, and time zone of the connection. # Configures the encoding, verbosity, schema search path, and time zone of the connection.

View File

@ -105,11 +105,6 @@ module ActiveRecord
@active = true @active = true
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
if sqlite_version < "3.8.0"
raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8."
end
configure_connection configure_connection
end end
@ -401,6 +396,12 @@ module ActiveRecord
end end
private private
def check_version
if sqlite_version < "3.8.0"
raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8."
end
end
def initialize_type_map(m = type_map) def initialize_type_map(m = type_map)
super super
register_class_with_limit m, %r(int)i, SQLite3Integer register_class_with_limit m, %r(int)i, SQLite3Integer

View File

@ -344,34 +344,22 @@ module ActiveRecord
# post = Post.allocate # post = Post.allocate
# post.init_with(coder) # post.init_with(coder)
# post.title # => 'hello world' # post.title # => 'hello world'
def init_with(coder) def init_with(coder, &block)
coder = LegacyYamlAdapter.convert(self.class, coder) coder = LegacyYamlAdapter.convert(self.class, coder)
@attributes = self.class.yaml_encoder.decode(coder) attributes = self.class.yaml_encoder.decode(coder)
init_with_attributes(attributes, coder["new_record"], &block)
init_internals
@new_record = coder["new_record"]
self.class.define_attribute_methods
yield self if block_given?
_run_find_callbacks
_run_initialize_callbacks
self
end end
## ##
# Initializer used for instantiating objects that have been read from the # Initialize an empty model object from +attributes+.
# database. +attributes+ should be an attributes object, and unlike the # +attributes+ should be an attributes object, and unlike the
# `initialize` method, no assignment calls are made per attribute. # `initialize` method, no assignment calls are made per attribute.
# #
# :nodoc: # :nodoc:
def init_from_db(attributes) def init_with_attributes(attributes, new_record = false)
init_internals init_internals
@new_record = false @new_record = new_record
@attributes = attributes @attributes = attributes
self.class.define_attribute_methods self.class.define_attribute_methods

View File

@ -388,6 +388,11 @@ module ActiveRecord
@column_names ||= columns.map(&:name) @column_names ||= columns.map(&:name)
end end
def symbol_column_to_string(name_symbol) # :nodoc:
@symbol_column_to_string_name_hash ||= column_names.index_by(&:to_sym)
@symbol_column_to_string_name_hash[name_symbol]
end
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
# and columns used for single table inheritance have been removed. # and columns used for single table inheritance have been removed.
def content_columns def content_columns
@ -477,6 +482,7 @@ module ActiveRecord
def reload_schema_from_cache def reload_schema_from_cache
@arel_table = nil @arel_table = nil
@column_names = nil @column_names = nil
@symbol_column_to_string_name_hash = nil
@attribute_types = nil @attribute_types = nil
@content_columns = nil @content_columns = nil
@default_attributes = nil @default_attributes = nil

View File

@ -426,7 +426,7 @@ module ActiveRecord
existing_record.assign_attributes(assignable_attributes) existing_record.assign_attributes(assignable_attributes)
association(association_name).initialize_attributes(existing_record) association(association_name).initialize_attributes(existing_record)
else else
method = "build_#{association_name}" method = :"build_#{association_name}"
if respond_to?(method) if respond_to?(method)
send(method, assignable_attributes) send(method, assignable_attributes)
else else

View File

@ -209,7 +209,7 @@ module ActiveRecord
# new instance of the class. Accepts only keys as strings. # new instance of the class. Accepts only keys as strings.
def instantiate_instance_of(klass, attributes, column_types = {}, &block) def instantiate_instance_of(klass, attributes, column_types = {}, &block)
attributes = klass.attributes_builder.build_from_database(attributes, column_types) attributes = klass.attributes_builder.build_from_database(attributes, column_types)
klass.allocate.init_from_db(attributes, &block) klass.allocate.init_with_attributes(attributes, &block)
end end
# Called by +instantiate+ to decide which class to use for a new # Called by +instantiate+ to decide which class to use for a new

View File

@ -25,6 +25,8 @@ require "models/user"
require "models/member" require "models/member"
require "models/membership" require "models/membership"
require "models/sponsor" require "models/sponsor"
require "models/lesson"
require "models/student"
require "models/country" require "models/country"
require "models/treaty" require "models/treaty"
require "models/vertex" require "models/vertex"
@ -275,7 +277,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_habtm_saving_multiple_relationships def test_habtm_saving_multiple_relationships
new_project = Project.new("name" => "Grimetime") new_project = Project.new("name" => "Grimetime")
amount_of_developers = 4 amount_of_developers = 4
developers = (0...amount_of_developers).collect { |i| Developer.create(name: "JME #{i}") }.reverse developers = (0...amount_of_developers).reverse_each.map { |i| Developer.create(name: "JME #{i}") }
new_project.developer_ids = [developers[0].id, developers[1].id] new_project.developer_ids = [developers[0].id, developers[1].id]
new_project.developers_with_callback_ids = [developers[2].id, developers[3].id] new_project.developers_with_callback_ids = [developers[2].id, developers[3].id]
@ -780,6 +782,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
end end
def test_singular_ids_are_reloaded_after_collection_concat
student = Student.create(name: "Alberto Almagro")
student.lesson_ids
lesson = Lesson.create(name: "DSI")
student.lessons << lesson
assert_includes student.lesson_ids, lesson.id
end
def test_scoped_find_on_through_association_doesnt_return_read_only_records def test_scoped_find_on_through_association_doesnt_return_read_only_records
tag = Post.find(1).tags.find_by_name("General") tag = Post.find(1).tags.find_by_name("General")

View File

@ -42,6 +42,20 @@ module ActiveRecord
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end end
test "cache_key for relation with custom select and limit" do
developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5)
developers_with_select = developers.select("developers.*")
last_developer_timestamp = developers.first.updated_at
assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers_with_select.cache_key)
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers_with_select.cache_key
assert_equal ActiveSupport::Digest.hexdigest(developers_with_select.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
test "cache_key for loaded relation" do test "cache_key for loaded relation" do
developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load
last_developer_timestamp = developers.first.updated_at last_developer_timestamp = developers.first.updated_at

View File

@ -1,3 +1,26 @@
* Deprecate `ActiveSupport::Multibyte::Chars.consumes?` in favor of `String#is_utf8?`.
*Francesco Rodríguez*
* Fix duration being rounded to a full second.
```
time = DateTime.parse("2018-1-1")
time += 0.51.seconds
```
Will now correctly add 0.51 second and not 1 full second.
*Edouard Chin*
* Deprecate `ActiveSupport::Multibyte::Unicode#normalize` and `ActiveSuppport::Multibyte::Chars#normalize`
in favor of `String#unicode_normalize`
*Francesco Rodríguez*
* Deprecate `ActiveSupport::Multibyte::Unicode#downcase/upcase/swapcase` in favor of
`String#downcase/upcase/swapcase`.
*Francesco Rodríguez*
* Add `ActiveSupport::ParameterFilter`. * Add `ActiveSupport::ParameterFilter`.
*Yoshiyuki Kinjo* *Yoshiyuki Kinjo*

View File

@ -411,8 +411,6 @@ module ActiveSupport
# to the cache. If you do not want to write the cache when the cache is # to the cache. If you do not want to write the cache when the cache is
# not found, use #read_multi. # not found, use #read_multi.
# #
# Options are passed to the underlying cache implementation.
#
# Returns a hash with the data for each of the names. For example: # Returns a hash with the data for each of the names. For example:
# #
# cache.write("bim", "bam") # cache.write("bim", "bam")
@ -422,6 +420,17 @@ module ActiveSupport
# # => { "bim" => "bam", # # => { "bim" => "bam",
# # "unknown_key" => "Fallback value for key: unknown_key" } # # "unknown_key" => "Fallback value for key: unknown_key" }
# #
# Options are passed to the underlying cache implementation. For example:
#
# cache.fetch_multi("fizz", expires_in: 5.seconds) do |key|
# "buzz"
# end
# # => {"fizz"=>"buzz"}
# cache.read("fizz")
# # => "buzz"
# sleep(6)
# cache.read("fizz")
# # => nil
def fetch_multi(*names) def fetch_multi(*names)
raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given? raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?

View File

@ -110,7 +110,7 @@ class DateTime
# instance time. Do not use this method in combination with x.months, use # instance time. Do not use this method in combination with x.months, use
# months_since instead! # months_since instead!
def since(seconds) def since(seconds)
self + Rational(seconds.round, 86400) self + Rational(seconds, 86400)
end end
alias :in :since alias :in :since

View File

@ -62,9 +62,9 @@ module ActiveSupport
raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String) raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
I18n.transliterate( I18n.transliterate(
ActiveSupport::Multibyte::Unicode.normalize( ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc),
ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c), replacement: replacement
replacement: replacement) )
end end
# Replaces special characters in a string so that it may be used as part of # Replaces special characters in a string so that it may be used as part of

View File

@ -17,7 +17,7 @@ module ActiveSupport #:nodoc:
# through the +mb_chars+ method. Methods which would normally return a # through the +mb_chars+ method. Methods which would normally return a
# String object now return a Chars object so methods can be chained. # String object now return a Chars object so methods can be chained.
# #
# 'The Perfect String '.mb_chars.downcase.strip.normalize # 'The Perfect String '.mb_chars.downcase.strip
# # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string"> # # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string">
# #
# Chars objects are perfectly interchangeable with String objects as long as # Chars objects are perfectly interchangeable with String objects as long as
@ -76,6 +76,11 @@ module ActiveSupport #:nodoc:
# Returns +true+ when the proxy class can handle the string. Returns # Returns +true+ when the proxy class can handle the string. Returns
# +false+ otherwise. # +false+ otherwise.
def self.consumes?(string) def self.consumes?(string)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Chars.consumes? is deprecated and will be
removed from Rails 6.1. Use string.is_utf8? instead.
MSG
string.encoding == Encoding::UTF_8 string.encoding == Encoding::UTF_8
end end
@ -108,7 +113,7 @@ module ActiveSupport #:nodoc:
# #
# 'Café'.mb_chars.reverse.to_s # => 'éfaC' # 'Café'.mb_chars.reverse.to_s # => 'éfaC'
def reverse def reverse
chars(Unicode.unpack_graphemes(@wrapped_string).reverse.flatten.pack("U*")) chars(@wrapped_string.scan(/\X/).reverse.join)
end end
# Limits the byte size of the string to a number of bytes without breaking # Limits the byte size of the string to a number of bytes without breaking
@ -120,40 +125,12 @@ module ActiveSupport #:nodoc:
slice(0...translate_offset(limit)) slice(0...translate_offset(limit))
end end
# Converts characters in the string to uppercase.
#
# 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?"
def upcase
chars Unicode.upcase(@wrapped_string)
end
# Converts characters in the string to lowercase.
#
# 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum"
def downcase
chars Unicode.downcase(@wrapped_string)
end
# Converts characters in the string to the opposite case.
#
# 'El Cañón'.mb_chars.swapcase.to_s # => "eL cAÑÓN"
def swapcase
chars Unicode.swapcase(@wrapped_string)
end
# Converts the first character to uppercase and the remainder to lowercase.
#
# 'über'.mb_chars.capitalize.to_s # => "Über"
def capitalize
(slice(0) || chars("")).upcase + (slice(1..-1) || chars("")).downcase
end
# Capitalizes the first letter of every word, when possible. # Capitalizes the first letter of every word, when possible.
# #
# "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró" # "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró"
# "日本語".mb_chars.titleize.to_s # => "日本語" # "日本語".mb_chars.titleize.to_s # => "日本語"
def titleize def titleize
chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.upcase($1) }) chars(downcase.to_s.gsub(/\b('?\S)/u) { $1.upcase })
end end
alias_method :titlecase, :titleize alias_method :titlecase, :titleize
@ -165,7 +142,24 @@ module ActiveSupport #:nodoc:
# <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
# ActiveSupport::Multibyte::Unicode.default_normalization_form # ActiveSupport::Multibyte::Unicode.default_normalization_form
def normalize(form = nil) def normalize(form = nil)
chars(Unicode.normalize(@wrapped_string, form)) form ||= Unicode.default_normalization_form
# See https://www.unicode.org/reports/tr15, Table 1
if alias_form = Unicode::NORMALIZATION_FORM_ALIASES[form]
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
removed from Rails 6.1. Use #unicode_normalize(:#{alias_form}) instead.
MSG
send(:unicode_normalize, alias_form)
else
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
removed from Rails 6.1. Use #unicode_normalize instead.
MSG
raise ArgumentError, "#{form} is not a valid normalization variant", caller
end
end end
# Performs canonical decomposition on all the characters. # Performs canonical decomposition on all the characters.
@ -189,7 +183,7 @@ module ActiveSupport #:nodoc:
# 'क्षि'.mb_chars.length # => 4 # 'क्षि'.mb_chars.length # => 4
# 'क्षि'.mb_chars.grapheme_length # => 3 # 'क्षि'.mb_chars.grapheme_length # => 3
def grapheme_length def grapheme_length
Unicode.unpack_graphemes(@wrapped_string).length @wrapped_string.scan(/\X/).length
end end
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
@ -205,7 +199,7 @@ module ActiveSupport #:nodoc:
to_s.as_json(options) to_s.as_json(options)
end end
%w(capitalize downcase reverse tidy_bytes upcase).each do |method| %w(reverse tidy_bytes).each do |method|
define_method("#{method}!") do |*args| define_method("#{method}!") do |*args|
@wrapped_string = send(method, *args).to_s @wrapped_string = send(method, *args).to_s
self self

View File

@ -6,10 +6,17 @@ module ActiveSupport
extend self extend self
# A list of all available normalization forms. # A list of all available normalization forms.
# See http://www.unicode.org/reports/tr15/tr15-29.html for more # See https://www.unicode.org/reports/tr15/tr15-29.html for more
# information about normalization. # information about normalization.
NORMALIZATION_FORMS = [:c, :kc, :d, :kd] NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
NORMALIZATION_FORM_ALIASES = { # :nodoc:
c: :nfc,
d: :nfd,
kc: :nfkc,
kd: :nfkd
}
# The Unicode version that is supported by the implementation # The Unicode version that is supported by the implementation
UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"] UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"]
@ -100,31 +107,34 @@ module ActiveSupport
# Default is ActiveSupport::Multibyte::Unicode.default_normalization_form. # Default is ActiveSupport::Multibyte::Unicode.default_normalization_form.
def normalize(string, form = nil) def normalize(string, form = nil)
form ||= @default_normalization_form form ||= @default_normalization_form
# See http://www.unicode.org/reports/tr15, Table 1
case form # See https://www.unicode.org/reports/tr15, Table 1
when :d if alias_form = NORMALIZATION_FORM_ALIASES[form]
string.unicode_normalize(:nfd) ActiveSupport::Deprecation.warn(<<-MSG.squish)
when :c ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be
string.unicode_normalize(:nfc) removed from Rails 6.1. Use String#unicode_normalize(:#{alias_form}) instead.
when :kd MSG
string.unicode_normalize(:nfkd)
when :kc string.unicode_normalize(alias_form)
string.unicode_normalize(:nfkc)
else else
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be
removed from Rails 6.1. Use String#unicode_normalize instead.
MSG
raise ArgumentError, "#{form} is not a valid normalization variant", caller raise ArgumentError, "#{form} is not a valid normalization variant", caller
end end
end end
def downcase(string) %w(downcase upcase swapcase).each do |method|
string.downcase define_method(method) do |string|
end ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Unicode##{method} is deprecated and
will be removed from Rails 6.1. Use String methods directly.
MSG
def upcase(string) string.send(method)
string.upcase end
end
def swapcase(string)
string.swapcase
end end
private private

View File

@ -152,8 +152,8 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2005, 2, 22, 11, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(3600) assert_equal DateTime.civil(2005, 2, 22, 11, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(3600)
assert_equal DateTime.civil(2005, 2, 24, 10, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2) assert_equal DateTime.civil(2005, 2, 24, 10, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2)
assert_equal DateTime.civil(2005, 2, 24, 11, 10, 35), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2 + 3600 + 25) assert_equal DateTime.civil(2005, 2, 24, 11, 10, 35), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2 + 3600 + 25)
assert_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333) assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333)
assert_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667) assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667)
end end
def test_change def test_change

View File

@ -157,6 +157,16 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal({ "foo" => "hello" }, JSON.parse(json)) assert_equal({ "foo" => "hello" }, JSON.parse(json))
end end
def test_struct_to_json_with_options_nested
klass = Struct.new(:foo, :bar)
struct = klass.new "hello", "world"
parent_struct = klass.new struct, "world"
json = parent_struct.to_json only: [:foo]
assert_equal({ "foo" => { "foo" => "hello" } }, JSON.parse(json))
end
def test_hash_should_pass_encoding_options_to_children_in_as_json def test_hash_should_pass_encoding_options_to_children_in_as_json
person = { person = {
name: "John", name: "John",

View File

@ -73,9 +73,15 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end end
def test_consumes_utf8_strings def test_consumes_utf8_strings
assert @proxy_class.consumes?(UNICODE_STRING) ActiveSupport::Deprecation.silence do
assert @proxy_class.consumes?(ASCII_STRING) assert @proxy_class.consumes?(UNICODE_STRING)
assert_not @proxy_class.consumes?(BYTE_STRING) assert @proxy_class.consumes?(ASCII_STRING)
assert_not @proxy_class.consumes?(BYTE_STRING)
end
end
def test_consumes_is_deprecated
assert_deprecated { @proxy_class.consumes?(UNICODE_STRING) }
end end
def test_concatenation_should_return_a_proxy_class_instance def test_concatenation_should_return_a_proxy_class_instance
@ -165,7 +171,9 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
assert chars("").upcase.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").upcase.kind_of?(ActiveSupport::Multibyte.proxy_class)
assert chars("").downcase.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").downcase.kind_of?(ActiveSupport::Multibyte.proxy_class)
assert chars("").capitalize.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").capitalize.kind_of?(ActiveSupport::Multibyte.proxy_class)
assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class) ActiveSupport::Deprecation.silence do
assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class)
end
assert chars("").decompose.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").decompose.kind_of?(ActiveSupport::Multibyte.proxy_class)
assert chars("").compose.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").compose.kind_of?(ActiveSupport::Multibyte.proxy_class)
assert chars("").tidy_bytes.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").tidy_bytes.kind_of?(ActiveSupport::Multibyte.proxy_class)
@ -383,10 +391,12 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
def test_reverse_should_work_with_normalized_strings def test_reverse_should_work_with_normalized_strings
str = "bös" str = "bös"
reversed_str = "söb" reversed_str = "söb"
assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse ActiveSupport::Deprecation.silence do
assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse
assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse
assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse
assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse
end
assert_equal chars(reversed_str).decompose, chars(str).decompose.reverse assert_equal chars(reversed_str).decompose, chars(str).decompose.reverse
assert_equal chars(reversed_str).compose, chars(str).compose.reverse assert_equal chars(reversed_str).compose, chars(str).compose.reverse
end end
@ -477,7 +487,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
def test_method_works_for_proxyed_methods def test_method_works_for_proxyed_methods
assert_equal "ll", "hello".mb_chars.method(:slice).call(2..3) # Defined on Chars assert_equal "ll", "hello".mb_chars.method(:slice).call(2..3) # Defined on Chars
chars = "hello".mb_chars chars = +"hello".mb_chars
assert_equal "Hello", chars.method(:capitalize!).call # Defined on Chars assert_equal "Hello", chars.method(:capitalize!).call # Defined on Chars
assert_equal "Hello", chars assert_equal "Hello", chars
assert_equal "jello", "hello".mb_chars.method(:gsub).call(/h/, "j") # Defined on String assert_equal "jello", "hello".mb_chars.method(:gsub).call(/h/, "j") # Defined on String
@ -568,7 +578,9 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
def test_composition_exclusion_is_set_up_properly def test_composition_exclusion_is_set_up_properly
# Normalization of DEVANAGARI LETTER QA breaks when composition exclusion isn't used correctly # Normalization of DEVANAGARI LETTER QA breaks when composition exclusion isn't used correctly
qa = [0x915, 0x93c].pack("U*") qa = [0x915, 0x93c].pack("U*")
assert_equal qa, chars(qa).normalize(:c) ActiveSupport::Deprecation.silence do
assert_equal qa, chars(qa).normalize(:c)
end
end end
# Test for the Public Review Issue #29, bad explanation of composition might lead to a # Test for the Public Review Issue #29, bad explanation of composition might lead to a
@ -578,17 +590,21 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
[0x0B47, 0x0300, 0x0B3E], [0x0B47, 0x0300, 0x0B3E],
[0x1100, 0x0300, 0x1161] [0x1100, 0x0300, 0x1161]
].map { |c| c.pack("U*") }.each do |c| ].map { |c| c.pack("U*") }.each do |c|
assert_equal_codepoints c, chars(c).normalize(:c) ActiveSupport::Deprecation.silence do
assert_equal_codepoints c, chars(c).normalize(:c)
end
end end
end end
def test_normalization_shouldnt_strip_null_bytes def test_normalization_shouldnt_strip_null_bytes
null_byte_str = "Test\0test" null_byte_str = "Test\0test"
assert_equal null_byte_str, chars(null_byte_str).normalize(:kc) ActiveSupport::Deprecation.silence do
assert_equal null_byte_str, chars(null_byte_str).normalize(:c) assert_equal null_byte_str, chars(null_byte_str).normalize(:kc)
assert_equal null_byte_str, chars(null_byte_str).normalize(:d) assert_equal null_byte_str, chars(null_byte_str).normalize(:c)
assert_equal null_byte_str, chars(null_byte_str).normalize(:kd) assert_equal null_byte_str, chars(null_byte_str).normalize(:d)
assert_equal null_byte_str, chars(null_byte_str).normalize(:kd)
end
assert_equal null_byte_str, chars(null_byte_str).decompose assert_equal null_byte_str, chars(null_byte_str).decompose
assert_equal null_byte_str, chars(null_byte_str).compose assert_equal null_byte_str, chars(null_byte_str).compose
end end
@ -601,11 +617,13 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
323 # COMBINING DOT BELOW 323 # COMBINING DOT BELOW
].pack("U*") ].pack("U*")
assert_equal_codepoints "", chars("").normalize ActiveSupport::Deprecation.silence do
assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s assert_equal_codepoints "", chars("").normalize
assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s
assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s
assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s
assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s
end
end end
def test_should_compute_grapheme_length def test_should_compute_grapheme_length
@ -719,6 +737,41 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
assert_equal BYTE_STRING.dup.mb_chars.class, ActiveSupport::Multibyte::Chars assert_equal BYTE_STRING.dup.mb_chars.class, ActiveSupport::Multibyte::Chars
end end
def test_unicode_normalize_deprecation
# String#unicode_normalize default form is `:nfc`, and
# different than Multibyte::Unicode default, `:nkfc`.
# Deprecation should suggest the right form if no params
# are given and default is used.
assert_deprecated(/unicode_normalize\(:nfkc\)/) do
ActiveSupport::Multibyte::Unicode.normalize("")
end
assert_deprecated(/unicode_normalize\(:nfd\)/) do
ActiveSupport::Multibyte::Unicode.normalize("", :d)
end
end
def test_chars_normalize_deprecation
# String#unicode_normalize default form is `:nfc`, and
# different than Multibyte::Unicode default, `:nkfc`.
# Deprecation should suggest the right form if no params
# are given and default is used.
assert_deprecated(/unicode_normalize\(:nfkc\)/) do
"".mb_chars.normalize
end
assert_deprecated(/unicode_normalize\(:nfc\)/) { "".mb_chars.normalize(:c) }
assert_deprecated(/unicode_normalize\(:nfd\)/) { "".mb_chars.normalize(:d) }
assert_deprecated(/unicode_normalize\(:nfkc\)/) { "".mb_chars.normalize(:kc) }
assert_deprecated(/unicode_normalize\(:nfkd\)/) { "".mb_chars.normalize(:kd) }
end
def test_unicode_deprecations
assert_deprecated { ActiveSupport::Multibyte::Unicode.downcase("") }
assert_deprecated { ActiveSupport::Multibyte::Unicode.upcase("") }
assert_deprecated { ActiveSupport::Multibyte::Unicode.swapcase("") }
end
private private
def string_from_classes(classes) def string_from_classes(classes)

View File

@ -18,64 +18,72 @@ class MultibyteConformanceTest < ActiveSupport::TestCase
end end
def test_normalizations_C def test_normalizations_C
each_line_of_norm_tests do |*cols| ActiveSupport::Deprecation.silence do
col1, col2, col3, col4, col5, comment = *cols each_line_of_norm_tests do |*cols|
col1, col2, col3, col4, col5, comment = *cols
# CONFORMANCE: # CONFORMANCE:
# 1. The following invariants must be true for all conformant implementations # 1. The following invariants must be true for all conformant implementations
# #
# NFC # NFC
# c2 == NFC(c1) == NFC(c2) == NFC(c3) # c2 == NFC(c1) == NFC(c2) == NFC(c3)
assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
# #
# c4 == NFC(c4) == NFC(c5) # c4 == NFC(c4) == NFC(c5)
assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
end
end end
end end
def test_normalizations_D def test_normalizations_D
each_line_of_norm_tests do |*cols| ActiveSupport::Deprecation.silence do
col1, col2, col3, col4, col5, comment = *cols each_line_of_norm_tests do |*cols|
# col1, col2, col3, col4, col5, comment = *cols
# NFD #
# c3 == NFD(c1) == NFD(c2) == NFD(c3) # NFD
assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" # c3 == NFD(c1) == NFD(c2) == NFD(c3)
assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
# c5 == NFD(c4) == NFD(c5) assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" # c5 == NFD(c4) == NFD(c5)
assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
end
end end
end end
def test_normalizations_KC def test_normalizations_KC
each_line_of_norm_tests do | *cols | ActiveSupport::Deprecation.silence do
col1, col2, col3, col4, col5, comment = *cols each_line_of_norm_tests do | *cols |
# col1, col2, col3, col4, col5, comment = *cols
# NFKC #
# c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) # NFKC
assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
end
end end
end end
def test_normalizations_KD def test_normalizations_KD
each_line_of_norm_tests do | *cols | ActiveSupport::Deprecation.silence do
col1, col2, col3, col4, col5, comment = *cols each_line_of_norm_tests do | *cols |
# col1, col2, col3, col4, col5, comment = *cols
# NFKD #
# c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) # NFKD
assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
end
end end
end end

View File

@ -18,64 +18,72 @@ class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase
end end
def test_normalizations_C def test_normalizations_C
each_line_of_norm_tests do |*cols| ActiveSupport::Deprecation.silence do
col1, col2, col3, col4, col5, comment = *cols each_line_of_norm_tests do |*cols|
col1, col2, col3, col4, col5, comment = *cols
# CONFORMANCE: # CONFORMANCE:
# 1. The following invariants must be true for all conformant implementations # 1. The following invariants must be true for all conformant implementations
# #
# NFC # NFC
# c2 == NFC(c1) == NFC(c2) == NFC(c3) # c2 == NFC(c1) == NFC(c2) == NFC(c3)
assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
# #
# c4 == NFC(c4) == NFC(c5) # c4 == NFC(c4) == NFC(c5)
assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
end
end end
end end
def test_normalizations_D def test_normalizations_D
each_line_of_norm_tests do |*cols| ActiveSupport::Deprecation.silence do
col1, col2, col3, col4, col5, comment = *cols each_line_of_norm_tests do |*cols|
# col1, col2, col3, col4, col5, comment = *cols
# NFD #
# c3 == NFD(c1) == NFD(c2) == NFD(c3) # NFD
assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" # c3 == NFD(c1) == NFD(c2) == NFD(c3)
assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
# c5 == NFD(c4) == NFD(c5) assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" # c5 == NFD(c4) == NFD(c5)
assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
end
end end
end end
def test_normalizations_KC def test_normalizations_KC
each_line_of_norm_tests do | *cols | ActiveSupport::Deprecation.silence do
col1, col2, col3, col4, col5, comment = *cols each_line_of_norm_tests do | *cols |
# col1, col2, col3, col4, col5, comment = *cols
# NFKC #
# c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) # NFKC
assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
end
end end
end end
def test_normalizations_KD def test_normalizations_KD
each_line_of_norm_tests do | *cols | ActiveSupport::Deprecation.silence do
col1, col2, col3, col4, col5, comment = *cols each_line_of_norm_tests do | *cols |
# col1, col2, col3, col4, col5, comment = *cols
# NFKD #
# c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) # NFKD
assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
end
end end
end end

View File

@ -166,7 +166,7 @@ NOTE: Support for parsing XML parameters has been extracted into a gem named `ac
The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id`, will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL: The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id`, will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL:
```ruby ```ruby
get '/clients/:status' => 'clients#index', foo: 'bar' get '/clients/:status', to: 'clients#index', foo: 'bar'
``` ```
In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar", as if it were passed in the query string. Your controller will also receive `params[:action]` as "index" and `params[:controller]` as "clients". In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar", as if it were passed in the query string. Your controller will also receive `params[:action]` as "index" and `params[:controller]` as "clients".

View File

@ -82,9 +82,9 @@ of two or more words, the model class name should follow the Ruby conventions,
using the CamelCase form, while the table name must contain the words separated using the CamelCase form, while the table name must contain the words separated
by underscores. Examples: by underscores. Examples:
* Database Table - Plural with underscores separating words (e.g., `book_clubs`).
* Model Class - Singular with the first letter of each word capitalized (e.g., * Model Class - Singular with the first letter of each word capitalized (e.g.,
`BookClub`). `BookClub`).
* Database Table - Plural with underscores separating words (e.g., `book_clubs`).
| Model / Class | Table / Schema | | Model / Class | Table / Schema |
| ---------------- | -------------- | | ---------------- | -------------- |

View File

@ -109,7 +109,7 @@ class CreateBooks < ActiveRecord::Migration[5.0]
end end
create_table :books do |t| create_table :books do |t|
t.belongs_to :author, index: true t.belongs_to :author
t.datetime :published_at t.datetime :published_at
t.timestamps t.timestamps
end end
@ -140,7 +140,7 @@ class CreateSuppliers < ActiveRecord::Migration[5.0]
end end
create_table :accounts do |t| create_table :accounts do |t|
t.belongs_to :supplier, index: true t.belongs_to :supplier
t.string :account_number t.string :account_number
t.timestamps t.timestamps
end end
@ -184,7 +184,7 @@ class CreateAuthors < ActiveRecord::Migration[5.0]
end end
create_table :books do |t| create_table :books do |t|
t.belongs_to :author, index: true t.belongs_to :author
t.datetime :published_at t.datetime :published_at
t.timestamps t.timestamps
end end
@ -231,8 +231,8 @@ class CreateAppointments < ActiveRecord::Migration[5.0]
end end
create_table :appointments do |t| create_table :appointments do |t|
t.belongs_to :physician, index: true t.belongs_to :physician
t.belongs_to :patient, index: true t.belongs_to :patient
t.datetime :appointment_date t.datetime :appointment_date
t.timestamps t.timestamps
end end
@ -312,13 +312,13 @@ class CreateAccountHistories < ActiveRecord::Migration[5.0]
end end
create_table :accounts do |t| create_table :accounts do |t|
t.belongs_to :supplier, index: true t.belongs_to :supplier
t.string :account_number t.string :account_number
t.timestamps t.timestamps
end end
create_table :account_histories do |t| create_table :account_histories do |t|
t.belongs_to :account, index: true t.belongs_to :account
t.integer :credit_rating t.integer :credit_rating
t.timestamps t.timestamps
end end
@ -358,8 +358,8 @@ class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
end end
create_table :assemblies_parts, id: false do |t| create_table :assemblies_parts, id: false do |t|
t.belongs_to :assembly, index: true t.belongs_to :assembly
t.belongs_to :part, index: true t.belongs_to :part
end end
end end
end end
@ -487,7 +487,7 @@ class CreatePictures < ActiveRecord::Migration[5.0]
def change def change
create_table :pictures do |t| create_table :pictures do |t|
t.string :name t.string :name
t.references :imageable, polymorphic: true, index: true t.references :imageable, polymorphic: true
t.timestamps t.timestamps
end end
end end
@ -517,7 +517,7 @@ In your migrations/schema, you will add a references column to the model itself.
class CreateEmployees < ActiveRecord::Migration[5.0] class CreateEmployees < ActiveRecord::Migration[5.0]
def change def change
create_table :employees do |t| create_table :employees do |t|
t.references :manager, index: true t.references :manager
t.timestamps t.timestamps
end end
end end

View File

@ -1266,7 +1266,7 @@ You can also pass in arbitrary local variables to any partial you are rendering
In this case, the partial will have access to a local variable `title` with the value "Products Page". In this case, the partial will have access to a local variable `title` with the value "Products Page".
TIP: Rails also makes a counter variable available within a partial called by the collection, named after the title of the partial followed by `_counter`. For example, when rendering a collection `@products` the partial `_product.html.erb` can access the variable `product_counter` which indexes the number of times it has been rendered within the enclosing view. TIP: Rails also makes a counter variable available within a partial called by the collection, named after the title of the partial followed by `_counter`. For example, when rendering a collection `@products` the partial `_product.html.erb` can access the variable `product_counter` which indexes the number of times it has been rendered within the enclosing view. Note that it also applies for when the partial name was changed by using the `as:` option. For example, the counter variable for the code above would be `item_counter`.
You can also specify a second partial to be rendered between instances of the main partial by using the `:spacer_template` option: You can also specify a second partial to be rendered between instances of the main partial by using the `:spacer_template` option:

View File

@ -1,3 +1,9 @@
* Use Ids instead of memory addresses when displaying references in scaffold views.
Fixes #29200.
*Rasesh Patel*
* Adds support for multiple databases to `rails db:migrate:status`. * Adds support for multiple databases to `rails db:migrate:status`.
Subtasks are also added to get the status of individual databases (eg. `rails db:migrate:status:animals`). Subtasks are also added to get the status of individual databases (eg. `rails db:migrate:status:animals`).

View File

@ -16,7 +16,7 @@
<%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %> <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %>
<tr> <tr>
<% attributes.reject(&:password_digest?).each do |attribute| -%> <% attributes.reject(&:password_digest?).each do |attribute| -%>
<td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td> <td><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></td>
<% end -%> <% end -%>
<td><%%= link_to 'Show', <%= model_resource_name %> %></td> <td><%%= link_to 'Show', <%= model_resource_name %> %></td>
<td><%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %></td> <td><%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %></td>

View File

@ -3,7 +3,7 @@
<% attributes.reject(&:password_digest?).each do |attribute| -%> <% attributes.reject(&:password_digest?).each do |attribute| -%>
<p> <p>
<strong><%= attribute.human_name %>:</strong> <strong><%= attribute.human_name %>:</strong>
<%%= @<%= singular_table_name %>.<%= attribute.name %> %> <%%= @<%= singular_table_name %>.<%= attribute.column_name %> %>
</p> </p>
<% end -%> <% end -%>

View File

@ -209,6 +209,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end end
def test_new_application_doesnt_need_defaults def test_new_application_doesnt_need_defaults
run_generator
assert_no_file "config/initializers/new_framework_defaults_6_0.rb" assert_no_file "config/initializers/new_framework_defaults_6_0.rb"
end end

View File

@ -435,8 +435,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end end
end end
def test_scaffold_generator_belongs_to def test_scaffold_generator_belongs_to_and_references
run_generator ["account", "name", "currency:belongs_to"] run_generator ["account", "name", "currency:belongs_to", "user:references"]
assert_file "app/models/account.rb", /belongs_to :currency/ assert_file "app/models/account.rb", /belongs_to :currency/
@ -449,7 +449,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_file "app/controllers/accounts_controller.rb" do |content| assert_file "app/controllers/accounts_controller.rb" do |content|
assert_instance_method :account_params, content do |m| assert_instance_method :account_params, content do |m|
assert_match(/permit\(:name, :currency_id\)/, m) assert_match(/permit\(:name, :currency_id, :user_id\)/, m)
end end
end end
@ -457,6 +457,16 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_match(/^\W{4}<%= form\.text_field :name %>/, content) assert_match(/^\W{4}<%= form\.text_field :name %>/, content)
assert_match(/^\W{4}<%= form\.text_field :currency_id %>/, content) assert_match(/^\W{4}<%= form\.text_field :currency_id %>/, content)
end end
assert_file "app/views/accounts/index.html.erb" do |content|
assert_match(/^\W{8}<td><%= account\.name %><\/td>/, content)
assert_match(/^\W{8}<td><%= account\.user_id %><\/td>/, content)
end
assert_file "app/views/accounts/show.html.erb" do |content|
assert_match(/^\W{2}<%= @account\.name %>/, content)
assert_match(/^\W{2}<%= @account\.user_id %>/, content)
end
end end
def test_scaffold_generator_database def test_scaffold_generator_database

View File

@ -16,6 +16,9 @@ require "active_support/testing/autorun"
require "active_support/testing/stream" require "active_support/testing/stream"
require "active_support/testing/method_call_assertions" require "active_support/testing/method_call_assertions"
require "active_support/test_case" require "active_support/test_case"
require "minitest/retry"
Minitest::Retry.use!(verbose: false, retry_count: 1)
RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__) RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__)