aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actioncable/test/stubs/test_adapter.rb2
-rw-r--r--activemodel/lib/active_model/errors.rb48
-rw-r--r--activemodel/lib/active_model/railtie.rb6
-rw-r--r--activemodel/test/cases/railtie_test.rb20
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb59
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb2
-rw-r--r--activestorage/app/models/active_storage/preview.rb4
-rw-r--r--activestorage/lib/active_storage/analyzer/video_analyzer.rb4
-rw-r--r--activestorage/lib/active_storage/previewer.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb27
-rw-r--r--guides/source/active_storage_overview.md11
-rw-r--r--guides/source/configuring.md4
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md6
-rw-r--r--guides/source/development_dependencies_install.md28
14 files changed, 187 insertions, 36 deletions
diff --git a/actioncable/test/stubs/test_adapter.rb b/actioncable/test/stubs/test_adapter.rb
index 5f23a137ea..3b25c9168f 100644
--- a/actioncable/test/stubs/test_adapter.rb
+++ b/actioncable/test/stubs/test_adapter.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class SuccessAdapter < ActionCable::SubscriptionAdapter::Base
- class << self; attr_accessor :subscribe_called, :unsubscribe_called end
-
def broadcast(channel, payload)
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 275e3f1313..d583d48b54 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -62,6 +62,11 @@ module ActiveModel
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
MESSAGE_OPTIONS = [:message]
+ class << self
+ attr_accessor :i18n_full_message # :nodoc:
+ end
+ self.i18n_full_message = false
+
attr_reader :messages, :details
# Pass in the instance of the object that is using the errors object.
@@ -364,12 +369,51 @@ module ActiveModel
# Returns a full message for a given attribute.
#
# person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
+ #
+ # The `"%{attribute} %{message}"` error format can be overridden with either
+ #
+ # * <tt>activemodel.errors.models.person/contacts/addresses.attributes.street.format</tt>
+ # * <tt>activemodel.errors.models.person/contacts/addresses.format</tt>
+ # * <tt>activemodel.errors.models.person.attributes.name.format</tt>
+ # * <tt>activemodel.errors.models.person.format</tt>
+ # * <tt>errors.format</tt>
def full_message(attribute, message)
return message if attribute == :base
+
+ if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope)
+ parts = attribute.to_s.split(".")
+ attribute_name = parts.pop
+ namespace = parts.join("/") unless parts.empty?
+ attributes_scope = "#{@base.class.i18n_scope}.errors.models"
+
+ if namespace
+ defaults = @base.class.lookup_ancestors.map do |klass|
+ [
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
+ ]
+ end
+ else
+ defaults = @base.class.lookup_ancestors.map do |klass|
+ [
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
+ ]
+ end
+ end
+
+ defaults.flatten!
+ else
+ defaults = []
+ end
+
+ defaults << :"errors.format"
+ defaults << "%{attribute} %{message}"
+
attr_name = attribute.to_s.tr(".", "_").humanize
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
- I18n.t(:"errors.format",
- default: "%{attribute} %{message}",
+ I18n.t(defaults.shift,
+ default: defaults,
attribute: attr_name,
message: message)
end
diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb
index a9cdabba00..0ed70bd473 100644
--- a/activemodel/lib/active_model/railtie.rb
+++ b/activemodel/lib/active_model/railtie.rb
@@ -7,8 +7,14 @@ module ActiveModel
class Railtie < Rails::Railtie # :nodoc:
config.eager_load_namespaces << ActiveModel
+ config.active_model = ActiveSupport::OrderedOptions.new
+
initializer "active_model.secure_password" do
ActiveModel::SecurePassword.min_cost = Rails.env.test?
end
+
+ initializer "active_model.i18n_full_message" do
+ ActiveModel::Errors.i18n_full_message = config.active_model.delete(:i18n_full_message) || false
+ end
end
end
diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb
index ff5022e960..ab60285e2a 100644
--- a/activemodel/test/cases/railtie_test.rb
+++ b/activemodel/test/cases/railtie_test.rb
@@ -31,4 +31,24 @@ class RailtieTest < ActiveModel::TestCase
assert_equal true, ActiveModel::SecurePassword.min_cost
end
+
+ test "i18n full message defaults to false" do
+ @app.initialize!
+
+ assert_equal false, ActiveModel::Errors.i18n_full_message
+ end
+
+ test "i18n full message can be disabled" do
+ @app.config.active_model.i18n_full_message = false
+ @app.initialize!
+
+ assert_equal false, ActiveModel::Errors.i18n_full_message
+ end
+
+ test "i18n full message can be enabled" do
+ @app.config.active_model.i18n_full_message = true
+ @app.initialize!
+
+ assert_equal true, ActiveModel::Errors.i18n_full_message
+ end
end
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index 9cfe189d0e..1c62e750de 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -12,6 +12,9 @@ class I18nValidationTest < ActiveModel::TestCase
I18n.load_path.clear
I18n.backend = I18n::Backend::Simple.new
I18n.backend.store_translations("en", errors: { messages: { custom: nil } })
+
+ @original_i18n_full_message = ActiveModel::Errors.i18n_full_message
+ ActiveModel::Errors.i18n_full_message = true
end
def teardown
@@ -19,6 +22,7 @@ class I18nValidationTest < ActiveModel::TestCase
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
I18n.backend.reload!
+ ActiveModel::Errors.i18n_full_message = @original_i18n_full_message
end
def test_full_message_encoding
@@ -42,6 +46,61 @@ class I18nValidationTest < ActiveModel::TestCase
assert_equal ["Field Name empty"], @person.errors.full_messages
end
+ def test_errors_full_messages_doesnt_use_attribute_format_without_config
+ ActiveModel::Errors.i18n_full_message = false
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "Name cannot be blank", person.errors.full_message(:name, "cannot be blank")
+ assert_equal "Name test cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
+ end
+
+ def test_errors_full_messages_uses_attribute_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank")
+ assert_equal "Name test cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
+ end
+
+ def test_errors_full_messages_uses_model_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { person: { format: "%{message}" } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank")
+ assert_equal "cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
+ end
+
+ def test_errors_full_messages_uses_deeply_nested_model_attributes_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.street', "cannot be blank")
+ assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_uses_deeply_nested_model_model_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.street', "cannot be blank")
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank")
+ end
+
# ActiveModel::Validations
# A set of common cases for ActiveModel::Validations message generation that
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 48cb819484..840d900bbc 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -400,7 +400,7 @@ module ActiveRecord
records.each { |record| callback(:before_remove, record) }
delete_records(existing_records, method) if existing_records.any?
- records.each { |record| target.delete(record) }
+ @target -= records
records.each { |record| callback(:after_remove, record) }
end
diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb
index de58763399..dd50494799 100644
--- a/activestorage/app/models/active_storage/preview.rb
+++ b/activestorage/app/models/active_storage/preview.rb
@@ -22,8 +22,8 @@
# Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
#
# The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
-# {ffmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
-# and the other requires {mupdf}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or mupdf.
+# {FFmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
+# and the other requires {muPDF}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or muPDF.
#
# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
# install and use third-party software, make sure you understand the licensing implications of doing so.
diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
index e31bdb0edb..18d8ff8237 100644
--- a/activestorage/lib/active_storage/analyzer/video_analyzer.rb
+++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
@@ -16,7 +16,7 @@ module ActiveStorage
#
# When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
#
- # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
class Analyzer::VideoAnalyzer < Analyzer
def self.accept?(blob)
blob.video?
@@ -107,7 +107,7 @@ module ActiveStorage
JSON.parse(output.read)
end
rescue Errno::ENOENT
- logger.info "Skipping video analysis because ffmpeg isn't installed"
+ logger.info "Skipping video analysis because FFmpeg isn't installed"
{}
end
diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb
index 13657bf13e..fb202f029a 100644
--- a/activestorage/lib/active_storage/previewer.rb
+++ b/activestorage/lib/active_storage/previewer.rb
@@ -31,7 +31,7 @@ module ActiveStorage
# Executes a system command, capturing its binary output in a tempfile. Yields the tempfile.
#
- # Use this method to shell out to a system library (e.g. mupdf or ffmpeg) for preview image
+ # Use this method to shell out to a system library (e.g. muPDF or FFmpeg) for preview image
# generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash:
#
# def preview
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
index 976a10d0f5..05abd83221 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -5,13 +5,13 @@ require "active_support/core_ext/object/try"
module DateAndTime
module Calculations
DAYS_INTO_WEEK = {
- monday: 0,
- tuesday: 1,
- wednesday: 2,
- thursday: 3,
- friday: 4,
- saturday: 5,
- sunday: 6
+ sunday: 0,
+ monday: 1,
+ tuesday: 2,
+ wednesday: 3,
+ thursday: 4,
+ friday: 5,
+ saturday: 6
}
WEEKEND_DAYS = [ 6, 0 ]
@@ -263,9 +263,8 @@ module DateAndTime
# Week is assumed to start on +start_day+, default is
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
def days_to_week_start(start_day = Date.beginning_of_week)
- start_day_number = DAYS_INTO_WEEK[start_day]
- current_day_number = wday != 0 ? wday - 1 : 6
- (current_day_number - start_day_number) % 7
+ start_day_number = DAYS_INTO_WEEK.fetch(start_day)
+ (wday - start_day_number) % 7
end
# Returns a new date/time representing the start of this week on the given day.
@@ -346,8 +345,7 @@ module DateAndTime
# today.next_occurring(:monday) # => Mon, 18 Dec 2017
# today.next_occurring(:thursday) # => Thu, 21 Dec 2017
def next_occurring(day_of_week)
- current_day_number = wday != 0 ? wday - 1 : 6
- from_now = DAYS_INTO_WEEK.fetch(day_of_week) - current_day_number
+ from_now = DAYS_INTO_WEEK.fetch(day_of_week) - wday
from_now += 7 unless from_now > 0
advance(days: from_now)
end
@@ -358,8 +356,7 @@ module DateAndTime
# today.prev_occurring(:monday) # => Mon, 11 Dec 2017
# today.prev_occurring(:thursday) # => Thu, 07 Dec 2017
def prev_occurring(day_of_week)
- current_day_number = wday != 0 ? wday - 1 : 6
- ago = current_day_number - DAYS_INTO_WEEK.fetch(day_of_week)
+ ago = wday - DAYS_INTO_WEEK.fetch(day_of_week)
ago += 7 unless ago > 0
advance(days: -ago)
end
@@ -374,7 +371,7 @@ module DateAndTime
end
def days_span(day)
- (DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7
+ (DAYS_INTO_WEEK.fetch(day) - DAYS_INTO_WEEK.fetch(Date.beginning_of_week)) % 7
end
def copy_time_to(other)
diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md
index cb9acdadcf..e7a94fc510 100644
--- a/guides/source/active_storage_overview.md
+++ b/guides/source/active_storage_overview.md
@@ -446,11 +446,12 @@ the box, Active Storage supports previewing videos and PDF documents.
</ul>
```
-WARNING: Extracting previews requires third-party applications, `ffmpeg` for
-video and `mutool` for PDFs. These libraries are not provided by Rails. You must
-install them yourself to use the built-in previewers. Before you install and use
-third-party software, make sure you understand the licensing implications of
-doing so.
+WARNING: Extracting previews requires third-party applications, `FFmpeg` for
+video and `muPDF` for PDFs, and on macOS also `XQuartz` and `Poppler`.
+These libraries are not provided by Rails. You must install them yourself to
+use the built-in previewers. Before you install and use third-party software,
+make sure you understand the licensing implications of doing so.
+
Direct Uploads
--------------
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 14cdebd62c..4c2dea6721 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -305,6 +305,10 @@ All these configuration options are delegated to the `I18n` library.
config.i18n.fallbacks.map = { az: :tr, da: [:de, :en] }
```
+### Configuring Active Model
+
+* `config.active_model.i18n_full_message` is a boolean value which controls whether the `full_message` error format can be overridden at the attribute or model level in the locale files
+
### Configuring Active Record
`config.active_record` includes a variety of configuration options:
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index ba5d7bbee8..6c0c7aefc1 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -374,12 +374,6 @@ You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` als
The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings.
-If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag:
-
-```bash
-$ RUBYOPT=-W0 bundle exec rake test
-```
-
### Updating the CHANGELOG
The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version.
diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md
index 50274d700b..fdd560ccf3 100644
--- a/guides/source/development_dependencies_install.md
+++ b/guides/source/development_dependencies_install.md
@@ -376,3 +376,31 @@ command inside of the `activestorage` directory to install the dependencies:
```bash
yarn install
```
+
+Extracting previews, tested in ActiveStorage's test suite requires third-party
+applications, `FFmpeg` for video and `muPDF` for PDFs, and on macOS also
+`XQuartz` and `Poppler`.. Without these applications installed, ActiveStorage
+tests will raise errors.
+
+On macOS you can run:
+
+```bash
+brew install ffmpeg
+brew cask install xquartz
+brew install mupdf-tools
+brew install poppler
+```
+
+On Ubuntu, you can run:
+
+```bash
+sudo apt-get update && install ffmpeg
+sudo apt-get update && install mupdf mupdf-tools
+```
+
+On Fedora or CentOS, just run:
+
+```bash
+sudo yum install ffmpeg
+sudo yum install mupdf
+```