aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionmailer/test/i18n_with_controller_test.rb5
-rw-r--r--actionpack/CHANGELOG.md9
-rw-r--r--actionpack/lib/action_controller/api.rb2
-rw-r--r--actionpack/lib/action_controller/base.rb2
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb6
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb12
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb6
-rw-r--r--actionpack/lib/action_controller/test_case.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb8
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb12
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb8
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb45
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb145
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb15
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb32
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb2
-rw-r--r--actionpack/test/controller/routing_test.rb6
-rw-r--r--actionpack/test/dispatch/request/multipart_params_parsing_test.rb11
-rw-r--r--actionpack/test/dispatch/response_test.rb7
-rw-r--r--actionpack/test/dispatch/session/test_session_test.rb10
-rw-r--r--actionpack/test/fixtures/multipart/utf8_filename10
-rw-r--r--actionview/CHANGELOG.md18
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb38
-rw-r--r--actionview/lib/action_view/digestor.rb2
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb14
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb12
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/javascript_helper.rb6
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb27
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb6
-rw-r--r--actionview/lib/action_view/path_set.rb9
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb2
-rw-r--r--actionview/lib/action_view/template.rb9
-rw-r--r--actionview/lib/action_view/template/resolver.rb14
-rw-r--r--actionview/test/fixtures/digestor/events/_completed.html.erb0
-rw-r--r--actionview/test/fixtures/digestor/events/index.html.erb1
-rw-r--r--actionview/test/template/digestor_test.rb37
-rw-r--r--activejob/CHANGELOG.md7
-rw-r--r--activejob/lib/active_job/base.rb2
-rw-r--r--activejob/lib/active_job/core.rb7
-rw-r--r--activejob/lib/active_job/queue_adapters.rb29
-rw-r--r--activejob/lib/active_job/test_helper.rb4
-rw-r--r--activejob/lib/active_job/translation.rb11
-rw-r--r--activejob/test/cases/job_serialization_test.rb17
-rw-r--r--activejob/test/cases/translation_test.rb20
-rw-r--r--activejob/test/integration/queuing_test.rb17
-rw-r--r--activejob/test/jobs/translated_hello_job.rb10
-rw-r--r--activejob/test/support/integration/dummy_app_template.rb6
-rw-r--r--activejob/test/support/integration/test_case_helpers.rb4
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb2
-rw-r--r--activerecord/CHANGELOG.md27
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/associations.rb16
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb12
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb10
-rw-r--r--activerecord/lib/active_record/base.rb1
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb2
-rw-r--r--activerecord/lib/active_record/fixtures.rb4
-rw-r--r--activerecord/lib/active_record/migration.rb4
-rw-r--r--activerecord/lib/active_record/relation.rb26
-rw-r--r--activerecord/test/cases/adapters/mysql2/quoting_test.rb4
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb7
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb2
-rw-r--r--activerecord/test/cases/collection_cache_key_test.rb70
-rw-r--r--activerecord/test/models/member_detail.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/hash/except.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb6
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb37
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb4
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb18
-rw-r--r--activesupport/lib/active_support/log_subscriber/test_helper.rb4
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb2
-rw-r--r--guides/Rakefile2
-rw-r--r--guides/source/action_mailer_basics.md2
-rw-r--r--guides/source/action_view_overview.md2
-rw-r--r--guides/source/active_job_basics.md13
-rw-r--r--guides/source/active_record_migrations.md18
-rw-r--r--guides/source/asset_pipeline.md8
-rw-r--r--guides/source/association_basics.md13
-rw-r--r--guides/source/caching_with_rails.md208
-rw-r--r--guides/source/configuring.md2
-rw-r--r--guides/source/credits.html.erb2
-rw-r--r--guides/source/documents.yaml4
-rw-r--r--guides/source/form_helpers.md5
-rw-r--r--guides/source/getting_started.md10
-rw-r--r--guides/source/routing.md16
-rw-r--r--guides/source/working_with_javascript_in_rails.md2
-rw-r--r--railties/CHANGELOG.md17
-rw-r--r--railties/lib/rails/application_controller.rb2
-rw-r--r--railties/lib/rails/commands/server.rb23
-rw-r--r--railties/lib/rails/engine.rb10
-rw-r--r--railties/lib/rails/generators/actions.rb4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/setup2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt14
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb31
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb1
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css3
-rw-r--r--railties/lib/rails/mailers_controller.rb2
-rw-r--r--railties/lib/rails/tasks.rb1
-rw-r--r--railties/lib/rails/tasks/dev.rake15
-rw-r--r--railties/lib/rails/test_help.rb4
-rw-r--r--railties/lib/rails/test_unit/minitest_plugin.rb1
-rw-r--r--railties/test/application/assets_test.rb1
-rw-r--r--railties/test/application/rake/dev_test.rb35
-rw-r--r--railties/test/application/rake/notes_test.rb1
-rw-r--r--railties/test/commands/server_test.rb16
-rw-r--r--railties/test/generators/plugin_generator_test.rb54
117 files changed, 1251 insertions, 326 deletions
diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb
index 010e44d045..5ffde06a80 100644
--- a/actionmailer/test/i18n_with_controller_test.rb
+++ b/actionmailer/test/i18n_with_controller_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'action_view'
require 'action_controller'
+require 'active_support/deprecation'
class I18nTestMailer < ActionMailer::Base
configure do |c|
@@ -54,7 +55,9 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest
def test_send_mail
Mail::SMTP.any_instance.expects(:deliver!)
with_translation 'de', email_subject: '[Anmeldung] Willkommen' do
- get '/test/send_mail'
+ ActiveSupport::Deprecation.silence do
+ get '/test/send_mail'
+ end
assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body
end
end
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 6a68c057ac..0e478abc97 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,12 @@
+* ActionController::TestSession now accepts a default value as well as
+ a block for generating a default value based off the key provided.
+
+ This fixes calls to session#fetch in ApplicationController instances that
+ take more two arguments or a block from raising `ArgumentError: wrong
+ number of arguments (2 for 1)` when performing controller tests.
+
+ *Matthew Gerrior*
+
* Fix `ActionController::Parameters#fetch` overwriting `KeyError` returned by
default block.
diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb
index 3af63b8892..b4594bf302 100644
--- a/actionpack/lib/action_controller/api.rb
+++ b/actionpack/lib/action_controller/api.rb
@@ -90,7 +90,7 @@ module ActionController
# Shortcut helper that returns all the ActionController::API modules except
# the ones passed as arguments:
#
- # class MetalController
+ # class MyAPIBaseController < ActionController::Metal
# ActionController::API.without_modules(:ForceSSL, :UrlFor).each do |left|
# include left
# end
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 2c3b3f4e05..17371a5392 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -183,7 +183,7 @@ module ActionController
# Shortcut helper that returns all the modules included in
# ActionController::Base except the ones passed as arguments:
#
- # class MetalController
+ # class MyBaseController < ActionController::Metal
# ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left|
# include left
# end
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index cdfc523bd4..e680432127 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -4,8 +4,8 @@ require 'active_support/core_ext/module/anonymous'
require 'action_dispatch/http/mime_type'
module ActionController
- # Wraps the parameters hash into a nested hash. This will allow clients to submit
- # POST requests without having to specify any root elements.
+ # Wraps the parameters hash into a nested hash. This will allow clients to
+ # submit requests without having to specify any root elements.
#
# This functionality is enabled in +config/initializers/wrap_parameters.rb+
# and can be customized.
@@ -14,7 +14,7 @@ module ActionController
# a non-empty array:
#
# class UsersController < ApplicationController
- # wrap_parameters format: [:json, :xml]
+ # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
# end
#
# If you enable +ParamsWrapper+ for +:json+ format, instead of having to
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 4cb634477e..d21a778d8d 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -20,7 +20,7 @@ module ActionController #:nodoc:
# Since HTML and JavaScript requests are typically made from the browser, we
# need to ensure to verify request authenticity for the web browser. We can
# use session-oriented authentication for these types of requests, by using
- # the `protect_form_forgery` method in our controllers.
+ # the `protect_from_forgery` method in our controllers.
#
# GET requests are not protected since they don't have side effects like writing
# to the database and don't leak sensitive information. JavaScript requests are
@@ -139,7 +139,7 @@ module ActionController #:nodoc:
request.session = NullSessionHash.new(request.env)
request.env['action_dispatch.request.flash_hash'] = nil
request.env['rack.session.options'] = { skip: true }
- request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
+ request.cookie_jar = NullCookieJar.build(request, {})
end
protected
@@ -160,14 +160,6 @@ module ActionController #:nodoc:
end
class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
- def self.build(request)
- key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
- host = request.host
- secure = request.ssl?
-
- new(key_generator, host, secure, options_for_env({}))
- end
-
def write(*)
# nothing
end
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
index 5a0e5c62e4..dbf7241a14 100644
--- a/actionpack/lib/action_controller/metal/url_for.rb
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -41,7 +41,11 @@ module ActionController
if original_script_name
options[:original_script_name] = original_script_name
else
- options[:script_name] = same_origin ? request.script_name.dup : script_name
+ if same_origin
+ options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup
+ else
+ options[:script_name] = script_name
+ end
end
options.freeze
else
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index f922e134f7..9dab72c5f0 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -171,6 +171,10 @@ module ActionController
clear
end
+ def fetch(*args, &block)
+ @data.fetch(*args, &block)
+ end
+
private
def load!
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index 6985cec5f5..a0d8f552ac 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -110,6 +110,10 @@ module ActionDispatch
env["action_dispatch.routes".freeze]
end
+ def routes=(routes) # :nodoc:
+ env["action_dispatch.routes".freeze] = routes
+ end
+
def original_script_name # :nodoc:
env['ORIGINAL_SCRIPT_NAME'.freeze]
end
@@ -118,6 +122,10 @@ module ActionDispatch
env[_routes.env_key]
end
+ def engine_script_name=(name) # :nodoc:
+ env[routes.env_key] = name.dup
+ end
+
def request_method=(request_method) #:nodoc:
if check_method(request_method)
@request_method = env["REQUEST_METHOD"] = request_method
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index eab7d0ab57..fd92e89231 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -197,13 +197,13 @@ module ActionDispatch # :nodoc:
@content_type = content_type.to_s
end
- # Sets the HTTP character set.
+ # Sets the HTTP character set. In case of nil parameter
+ # it sets the charset to utf-8.
+ #
+ # response.charset = 'utf-16' # => 'utf-16'
+ # response.charset = nil # => 'utf-8'
def charset=(charset)
- if nil == charset
- @charset = self.class.default_charset
- else
- @charset = charset
- end
+ @charset = charset.nil? ? self.class.default_charset : charset
end
# The response code of the request.
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index 540e11a4a0..a221f4c5af 100644
--- a/actionpack/lib/action_dispatch/http/upload.rb
+++ b/actionpack/lib/action_dispatch/http/upload.rb
@@ -28,7 +28,13 @@ module ActionDispatch
raise(ArgumentError, ':tempfile is required') unless @tempfile
@original_filename = hash[:filename]
- @original_filename &&= @original_filename.encode "UTF-8"
+ if @original_filename
+ begin
+ @original_filename.encode!(Encoding::UTF_8)
+ rescue EncodingError
+ @original_filename.force_encoding(Encoding::UTF_8)
+ end
+ end
@content_type = hash[:type]
@headers = hash[:head]
end
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index c0566c6fc9..d8bb10ffab 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -14,7 +14,7 @@ module ActionDispatch
def generate(name, options, path_parameters, parameterize = nil)
constraints = path_parameters.merge(options)
- missing_keys = []
+ missing_keys = nil # need for variable scope
match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
@@ -25,22 +25,22 @@ module ActionDispatch
next unless name || route.dispatcher?
missing_keys = missing_keys(route, parameterized_parts)
- next unless missing_keys.empty?
+ next if missing_keys && !missing_keys.empty?
params = options.dup.delete_if do |key, _|
parameterized_parts.key?(key) || route.defaults.key?(key)
end
defaults = route.defaults
required_parts = route.required_parts
- parameterized_parts.delete_if do |key, value|
- value.to_s == defaults[key].to_s && !required_parts.include?(key)
+ parameterized_parts.keep_if do |key, value|
+ defaults[key].nil? || value.to_s != defaults[key].to_s || required_parts.include?(key)
end
return [route.format(parameterized_parts), params]
end
message = "No route matches #{Hash[constraints.sort_by{|k,v| k.to_s}].inspect}"
- message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty?
+ message << " missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
raise ActionController::UrlGenerationError, message
end
@@ -54,12 +54,12 @@ module ActionDispatch
def extract_parameterized_parts(route, options, recall, parameterize = nil)
parameterized_parts = recall.merge(options)
- keys_to_keep = route.parts.reverse.drop_while { |part|
+ keys_to_keep = route.parts.reverse_each.drop_while { |part|
!options.key?(part) || (options[part] || recall[part]).nil?
} | route.required_parts
- (parameterized_parts.keys - keys_to_keep).each do |bad_key|
- parameterized_parts.delete(bad_key)
+ parameterized_parts.delete_if do |bad_key, _|
+ !keys_to_keep.include?(bad_key)
end
if parameterize
@@ -110,15 +110,36 @@ module ActionDispatch
routes
end
+ module RegexCaseComparator
+ DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/
+ DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/
+
+ def self.===(regex)
+ DEFAULT_INPUT == regex
+ end
+ end
+
# Returns an array populated with missing keys if any are present.
def missing_keys(route, parts)
- missing_keys = []
+ missing_keys = nil
tests = route.path.requirements
route.required_parts.each { |key|
- if tests.key?(key)
- missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key]
+ case tests[key]
+ when nil
+ unless parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
+ when RegexCaseComparator
+ unless RegexCaseComparator::DEFAULT_REGEX === parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
else
- missing_keys << key unless parts[key]
+ unless /\A#{tests[key]}\Z/ === parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
end
}
missing_keys
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 07d97bd6bd..cf4f654ed6 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -8,8 +8,50 @@ require 'active_support/json'
module ActionDispatch
class Request < Rack::Request
def cookie_jar
- env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(env, host, ssl?, cookies)
+ env['action_dispatch.cookies'.freeze] ||= Cookies::CookieJar.build(self, cookies)
end
+
+ # :stopdoc:
+ def have_cookie_jar?
+ env.key? 'action_dispatch.cookies'.freeze
+ end
+
+ def cookie_jar=(jar)
+ env['action_dispatch.cookies'.freeze] = jar
+ end
+
+ def key_generator
+ env[Cookies::GENERATOR_KEY]
+ end
+
+ def signed_cookie_salt
+ env[Cookies::SIGNED_COOKIE_SALT]
+ end
+
+ def encrypted_cookie_salt
+ env[Cookies::ENCRYPTED_COOKIE_SALT]
+ end
+
+ def encrypted_signed_cookie_salt
+ env[Cookies::ENCRYPTED_SIGNED_COOKIE_SALT]
+ end
+
+ def secret_token
+ env[Cookies::SECRET_TOKEN]
+ end
+
+ def secret_key_base
+ env[Cookies::SECRET_KEY_BASE]
+ end
+
+ def cookies_serializer
+ env[Cookies::COOKIES_SERIALIZER]
+ end
+
+ def cookies_digest
+ env[Cookies::COOKIES_DIGEST]
+ end
+ # :startdoc:
end
# \Cookies are read and written through ActionController#cookies.
@@ -118,7 +160,7 @@ module ActionDispatch
# cookies.permanent.signed[:remember_me] = current_user.id
# # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
def permanent
- @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
+ @permanent ||= PermanentCookieJar.new(self)
end
# Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
@@ -138,10 +180,10 @@ module ActionDispatch
# cookies.signed[:discount] # => 45
def signed
@signed ||=
- if @options[:upgrade_legacy_signed_cookies]
- UpgradeLegacySignedCookieJar.new(self, @key_generator, @options)
+ if upgrade_legacy_signed_cookies?
+ UpgradeLegacySignedCookieJar.new(self)
else
- SignedCookieJar.new(self, @key_generator, @options)
+ SignedCookieJar.new(self)
end
end
@@ -161,10 +203,10 @@ module ActionDispatch
# cookies.encrypted[:discount] # => 45
def encrypted
@encrypted ||=
- if @options[:upgrade_legacy_signed_cookies]
- UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options)
+ if upgrade_legacy_signed_cookies?
+ UpgradeLegacyEncryptedCookieJar.new(self)
else
- EncryptedCookieJar.new(self, @key_generator, @options)
+ EncryptedCookieJar.new(self)
end
end
@@ -172,12 +214,26 @@ module ActionDispatch
# Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
def signed_or_encrypted
@signed_or_encrypted ||=
- if @options[:secret_key_base].present?
+ if request.secret_key_base.present?
encrypted
else
signed
end
end
+
+ protected
+
+ def request; @parent_jar.request; end
+
+ private
+
+ def upgrade_legacy_signed_cookies?
+ request.secret_token.present? && request.secret_key_base.present?
+ end
+
+ def key_generator
+ request.key_generator
+ end
end
# Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
@@ -187,7 +243,7 @@ module ActionDispatch
module VerifyAndUpgradeLegacySignedMessage # :nodoc:
def initialize(*args)
super
- @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def verify_and_upgrade_legacy_signed_message(name, signed_message)
@@ -216,34 +272,18 @@ module ActionDispatch
# $& => example.local
DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
- def self.options_for_env(env) #:nodoc:
- { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '',
- encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '',
- encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
- secret_token: env[SECRET_TOKEN],
- secret_key_base: env[SECRET_KEY_BASE],
- upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?,
- serializer: env[COOKIES_SERIALIZER],
- digest: env[COOKIES_DIGEST]
- }
- end
-
- def self.build(env, host, secure, cookies)
- key_generator = env[GENERATOR_KEY]
- options = options_for_env env
-
- new(key_generator, host, secure, options).tap do |hash|
+ def self.build(req, cookies)
+ new(req).tap do |hash|
hash.update(cookies)
end
end
- def initialize(key_generator, host = nil, secure = false, options = {})
- @key_generator = key_generator
+ attr_reader :request
+
+ def initialize(request)
@set_cookies = {}
@delete_cookies = {}
- @host = host
- @secure = secure
- @options = options
+ @request = request
@cookies = {}
@committed = false
end
@@ -292,12 +332,12 @@ module ActionDispatch
# if host is not ip and matches domain regexp
# (ip confirms to domain regexp so we explicitly check for ip)
- options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp)
+ options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
".#{$&}"
end
elsif options[:domain].is_a? Array
# if host matches one of the supplied domains without a dot in front of it
- options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') }
+ options[:domain] = options[:domain].find {|domain| request.host.include? domain.sub(/^\./, '') }
end
end
@@ -356,27 +396,20 @@ module ActionDispatch
@delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
end
- def recycle! #:nodoc:
- @set_cookies = {}
- @delete_cookies = {}
- end
-
mattr_accessor :always_write_cookie
self.always_write_cookie = false
private
def write_cookie?(cookie)
- @secure || !cookie[:secure] || always_write_cookie
+ request.ssl? || !cookie[:secure] || always_write_cookie
end
end
class PermanentCookieJar #:nodoc:
include ChainedCookieJars
- def initialize(parent_jar, key_generator, options = {})
+ def initialize(parent_jar)
@parent_jar = parent_jar
- @key_generator = key_generator
- @options = options
end
def [](name)
@@ -410,7 +443,7 @@ module ActionDispatch
protected
def needs_migration?(value)
- @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
+ request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
end
def serialize(value)
@@ -430,7 +463,7 @@ module ActionDispatch
end
def serializer
- serializer = @options[:serializer] || :marshal
+ serializer = request.cookies_serializer || :marshal
case serializer
when :marshal
Marshal
@@ -442,7 +475,7 @@ module ActionDispatch
end
def digest
- @options[:digest] || 'SHA1'
+ request.cookies_digest || 'SHA1'
end
end
@@ -450,10 +483,9 @@ module ActionDispatch
include ChainedCookieJars
include SerializedCookieJars
- def initialize(parent_jar, key_generator, options = {})
+ def initialize(parent_jar)
@parent_jar = parent_jar
- @options = options
- secret = key_generator.generate_key(@options[:signed_cookie_salt])
+ secret = key_generator.generate_key(request.signed_cookie_salt)
@verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
@@ -505,16 +537,16 @@ module ActionDispatch
include ChainedCookieJars
include SerializedCookieJars
- def initialize(parent_jar, key_generator, options = {})
+ def initialize(parent_jar)
+ @parent_jar = parent_jar
+
if ActiveSupport::LegacyKeyGenerator === key_generator
raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +
"Read the upgrade documentation to learn more about this new config option."
end
- @parent_jar = parent_jar
- @options = options
- secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
- sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
+ secret = key_generator.generate_key(request.encrypted_cookie_salt || '')
+ sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '')
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
@@ -568,9 +600,12 @@ module ActionDispatch
end
def call(env)
+ request = ActionDispatch::Request.new env
+
status, headers, body = @app.call(env)
- if cookie_jar = env['action_dispatch.cookies']
+ if request.have_cookie_jar?
+ cookie_jar = request.cookie_jar
unless cookie_jar.committed?
cookie_jar.write(headers)
if headers[HTTP_HEADER].respond_to?(:join)
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index ec530c6e8a..64352d5742 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -443,6 +443,21 @@ module ActionDispatch
# dynamic segment used to generate the routes).
# You can access that segment from your controller using
# <tt>params[<:param>]</tt>.
+ # In your router:
+ #
+ # resources :user, param: :name
+ #
+ # You can override <tt>ActiveRecord::Base#to_param</tt> of a related
+ # model to constructe an URL.
+ #
+ # class User < ActiveRecord::Base
+ # def to_param # overridden
+ # name
+ # end
+ # end
+ #
+ # user = User.find_by(name: 'Phusion')
+ # user_path(user) # => "/users/Phusion"
#
# [:path]
# The path prefix for the routes.
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 42512cad91..f3d7a234d7 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -267,9 +267,13 @@ module ActionDispatch
path_params -= controller_options.keys
path_params -= result.keys
end
- path_params -= inner_options.keys
- path_params.take(args.size).each do |param|
- result[param] = args.shift
+ inner_options.each do |key, _|
+ path_params.delete(key)
+ end
+
+ args.each_with_index do |arg, index|
+ param = path_params[index]
+ result[param] = arg if param
end
end
@@ -583,10 +587,8 @@ module ActionDispatch
PARAMETERIZE = lambda do |name, value|
if name == :controller
value
- elsif value.is_a?(Array)
- value.map(&:to_param).join('/')
- elsif param = value.to_param
- param
+ else
+ value.to_param
end
end
@@ -594,8 +596,8 @@ module ActionDispatch
def initialize(named_route, options, recall, set)
@named_route = named_route
- @options = options.dup
- @recall = recall.dup
+ @options = options
+ @recall = recall
@set = set
normalize_recall!
@@ -617,7 +619,7 @@ module ActionDispatch
def use_recall_for(key)
if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
if !named_route_exists? || segment_keys.include?(key)
- @options[key] = @recall.delete(key)
+ @options[key] = @recall[key]
end
end
end
@@ -671,12 +673,18 @@ module ActionDispatch
# Remove leading slashes from controllers
def normalize_controller!
- @options[:controller] = controller.sub(%r{^/}, ''.freeze) if controller
+ if controller
+ if controller.start_with?("/".freeze)
+ @options[:controller] = controller[1..-1]
+ else
+ @options[:controller] = controller
+ end
+ end
end
# Move 'index' action from options to recall
def normalize_action!
- if @options[:action] == 'index'
+ if @options[:action] == 'index'.freeze
@recall[:action] = @options.delete(:action)
end
end
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index dc664d5540..0298962409 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -391,7 +391,7 @@ module ActionDispatch
attr_reader :app
- def before_setup
+ def before_setup # :nodoc:
@app = nil
@integration_session = nil
super
diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb
index 494644cd46..c28d701b48 100644
--- a/actionpack/lib/action_dispatch/testing/test_process.rb
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -19,7 +19,7 @@ module ActionDispatch
end
def cookies
- @cookie_jar ||= Cookies::CookieJar.build(@request.env, @request.host, @request.ssl?, @request.cookies)
+ @cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies)
end
def redirect_to_url
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index 9bbfb74e72..a333290ade 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -335,9 +335,11 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_route_with_colon_first
rs.draw do
- get '/:controller/:action/:id', :action => 'index', :id => nil
- get ':url', :controller => 'tiny_url', :action => 'translate'
+ get '/:controller/:action/:id', action: 'index', id: nil
+ get ':url', controller: 'content', action: 'translate'
end
+
+ assert_equal({controller: 'content', action: 'translate', url: 'example'}, rs.recognize_path('/example'))
end
def test_route_with_regexp_for_controller
diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
index 939a771c65..b36fbd3c76 100644
--- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
@@ -63,6 +63,17 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
assert_equal 'contents', file.read
end
+ test "parses utf8 filename with percent character" do
+ params = parse_multipart('utf8_filename')
+ assert_equal %w(file foo), params.keys.sort
+ assert_equal 'bar', params['foo']
+
+ file = params['file']
+ assert_equal 'ファイル%名.txt', file.original_filename
+ assert_equal "text/plain", file.content_type
+ assert_equal 'contents', file.read
+ end
+
test "parses boundary problem file" do
params = parse_multipart('boundary_problem_file')
assert_equal %w(file foo), params.keys.sort
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index 5aa0215e8f..780e7dc3e2 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -42,6 +42,13 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal Encoding::UTF_8, response.body.encoding
end
+ def test_response_charset_writer
+ @response.charset = 'utf-16'
+ assert_equal 'utf-16', @response.charset
+ @response.charset = nil
+ assert_equal 'utf-8', @response.charset
+ end
+
test "simple output" do
@response.body = "Hello, World!"
diff --git a/actionpack/test/dispatch/session/test_session_test.rb b/actionpack/test/dispatch/session/test_session_test.rb
index d30461a623..59c030176b 100644
--- a/actionpack/test/dispatch/session/test_session_test.rb
+++ b/actionpack/test/dispatch/session/test_session_test.rb
@@ -40,4 +40,14 @@ class ActionController::TestSessionTest < ActiveSupport::TestCase
assert_equal %w(one two), session.keys
assert_equal %w(1 2), session.values
end
+
+ def test_fetch_returns_default
+ session = ActionController::TestSession.new(one: '1')
+ assert_equal('2', session.fetch(:two, '2'))
+ end
+
+ def test_fetch_returns_block_value
+ session = ActionController::TestSession.new(one: '1')
+ assert_equal(2, session.fetch('2') { |key| key.to_i })
+ end
end
diff --git a/actionpack/test/fixtures/multipart/utf8_filename b/actionpack/test/fixtures/multipart/utf8_filename
new file mode 100644
index 0000000000..60738d53b0
--- /dev/null
+++ b/actionpack/test/fixtures/multipart/utf8_filename
@@ -0,0 +1,10 @@
+--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="file"; filename="ファイル%名.txt"
+Content-Type: text/plain
+
+contents
+--AaB03x--
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 1f6bb31cd4..55a2e155d1 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,21 @@
+* Add wildcard matching to explicit dependencies.
+
+ Turns:
+
+ ```erb
+ <% # Template Dependency: recordings/threads/events/subscribers_changed %>
+ <% # Template Dependency: recordings/threads/events/completed %>
+ <% # Template Dependency: recordings/threads/events/uncompleted %>
+ ```
+
+ Into:
+
+ ```erb
+ <% # Template Dependency: recordings/threads/events/* %>
+ ```
+
+ *Kasper Timm Hansen*
+
* Allow defining explicit collection caching using a `# Template Collection: ...`
directive inside templates.
diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb
index 7a7e116dbb..6e8c7f8203 100644
--- a/actionview/lib/action_view/dependency_tracker.rb
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -1,16 +1,18 @@
require 'thread_safe'
+require 'action_view/path_set'
module ActionView
class DependencyTracker # :nodoc:
@trackers = ThreadSafe::Cache.new
- def self.find_dependencies(name, template)
+ def self.find_dependencies(name, template, view_paths = nil)
tracker = @trackers[template.handler]
+ return [] unless tracker.present?
- if tracker.present?
- tracker.call(name, template)
+ if tracker.respond_to?(:supports_view_paths?) && tracker.supports_view_paths?
+ tracker.call(name, template, view_paths)
else
- []
+ tracker.call(name, template)
end
end
@@ -82,12 +84,16 @@ module ActionView
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
/xm
- def self.call(name, template)
- new(name, template).dependencies
+ def self.supports_view_paths? # :nodoc:
+ true
+ end
+
+ def self.call(name, template, view_paths = nil)
+ new(name, template, view_paths).dependencies
end
- def initialize(name, template)
- @name, @template = name, template
+ def initialize(name, template, view_paths = nil)
+ @name, @template, @view_paths = name, template, view_paths
end
def dependencies
@@ -142,8 +148,22 @@ module ActionView
end
end
+ def resolve_directories(wildcard_dependencies)
+ return [] unless @view_paths
+
+ wildcard_dependencies.each_with_object([]) do |query, templates|
+ @view_paths.find_all_with_query(query).each do |template|
+ templates << "#{File.dirname(query)}/#{File.basename(template).split('.').first}"
+ end
+ end
+ end
+
def explicit_dependencies
- source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
+ dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
+
+ wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == '*' }
+
+ (explicits + resolve_directories(wildcards)).uniq
end
end
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 4224346a2f..9a8a4feb2e 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -72,7 +72,7 @@ module ActionView
end
def dependencies
- DependencyTracker.find_dependencies(name, template)
+ DependencyTracker.find_dependencies(name, template, finder.view_paths)
rescue ActionView::MissingTemplate
logger.try :error, " '#{name}' file doesn't exist, so no dependencies"
[]
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index 3041ad3ef7..e506c782d6 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -60,7 +60,7 @@ module ActionView
tag_options = {
"src" => path_to_javascript(source, path_options)
}.merge!(options)
- content_tag(:script, "", tag_options)
+ content_tag("script".freeze, "", tag_options)
}.join("\n").html_safe
end
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index 797d029317..636d7d4cc3 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -98,7 +98,19 @@ module ActionView
# <%# Template Dependency: todolists/todolist %>
# <%= render_sortable_todolists @project.todolists %>
#
- # The pattern used to match these is <tt>/# Template Dependency: (\S+)/</tt>,
+ # In some cases, like a single table inheritance setup, you might have
+ # a bunch of explicit dependencies. Instead of writing every template out,
+ # you can use a wildcard to match any template in a directory:
+ #
+ # <%# Template Dependency: events/* %>
+ # <%= render_categorizable_events @person.events %>
+ #
+ # This marks every template in the directory as a dependency. To find those
+ # templates, the wildcard path must be absolutely defined from app/views or paths
+ # otherwise added with +prepend_view_path+ or +append_view_path+.
+ # This way the wildcard for `app/views/recordings/events` would be `recordings/events/*` etc.
+ #
+ # The pattern used to match explicit dependencies is <tt>/# Template Dependency: (\S+)/</tt>,
# so it's important that you type it out just so.
# You can only declare one template dependency per line.
#
diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb
index fbd7261477..94fbaa2dca 100644
--- a/actionview/lib/action_view/helpers/date_helper.rb
+++ b/actionview/lib/action_view/helpers/date_helper.rb
@@ -681,7 +681,7 @@ module ActionView
content = args.first || I18n.l(date_or_time, :format => format)
datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601
- content_tag(:time, content, options.reverse_merge(:datetime => datetime), &block)
+ content_tag("time".freeze, content, options.reverse_merge(:datetime => datetime), &block)
end
end
@@ -809,7 +809,7 @@ module ActionView
1.upto(12) do |month_number|
options = { :value => month_number }
options[:selected] = "selected" if month == month_number
- month_options << content_tag(:option, month_name(month_number), options) + "\n"
+ month_options << content_tag("option".freeze, month_name(month_number), options) + "\n"
end
build_select(:month, month_options.join)
end
@@ -971,7 +971,7 @@ module ActionView
tag_options[:selected] = "selected" if selected == i
text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value
text = options[:ampm] ? AMPM_TRANSLATION[i] : text
- select_options << content_tag(:option, text, tag_options)
+ select_options << content_tag("option".freeze, text, tag_options)
end
(select_options.join("\n") + "\n").html_safe
@@ -991,11 +991,11 @@ module ActionView
select_options[:class] = [select_options[:class], type].compact.join(' ') if @options[:with_css_classes]
select_html = "\n"
- select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
+ select_html << content_tag("option".freeze, '', :value => '') + "\n" if @options[:include_blank]
select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
select_html << select_options_as_html
- (content_tag(:select, select_html.html_safe, select_options) + "\n").html_safe
+ (content_tag("select".freeze, select_html.html_safe, select_options) + "\n").html_safe
end
# Builds a prompt option tag with supplied options or from default options.
@@ -1012,7 +1012,7 @@ module ActionView
I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale])
end
- prompt ? content_tag(:option, prompt, :value => '') : ''
+ prompt ? content_tag("option".freeze, prompt, :value => '') : ''
end
# Builds hidden input tag for date part and value.
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
index 8e729b3c39..728e633bbf 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -456,7 +456,7 @@ module ActionView
option_tags = options_from_collection_for_select(
group.send(group_method), option_key_method, option_value_method, selected_key)
- content_tag(:optgroup, option_tags, label: group.send(group_label_method))
+ content_tag("optgroup".freeze, option_tags, label: group.send(group_label_method))
end.join.html_safe
end
@@ -528,7 +528,7 @@ module ActionView
body = "".html_safe
if prompt
- body.safe_concat content_tag(:option, prompt_text(prompt), value: "")
+ body.safe_concat content_tag("option".freeze, prompt_text(prompt), value: "")
end
grouped_options.each do |container|
@@ -541,7 +541,7 @@ module ActionView
end
html_attributes = { label: label }.merge!(html_attributes)
- body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), html_attributes)
+ body.safe_concat content_tag("optgroup".freeze, options_for_select(container, selected_key), html_attributes)
end
body
@@ -577,7 +577,7 @@ module ActionView
end
zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
- zone_options.safe_concat content_tag(:option, '-------------', value: '', disabled: true)
+ zone_options.safe_concat content_tag("option".freeze, '-------------', value: '', disabled: true)
zone_options.safe_concat "\n"
zones = zones - priority_zones
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index 896020bc15..d36701955a 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -140,15 +140,15 @@ module ActionView
end
if include_blank
- option_tags = content_tag(:option, include_blank, value: '').safe_concat(option_tags)
+ option_tags = content_tag("option".freeze, include_blank, value: '').safe_concat(option_tags)
end
end
if prompt = options.delete(:prompt)
- option_tags = content_tag(:option, prompt, value: '').safe_concat(option_tags)
+ option_tags = content_tag("option".freeze, prompt, value: '').safe_concat(option_tags)
end
- content_tag :select, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
+ content_tag "select".freeze, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
end
# Creates a standard text field; use these text fields to input smaller chunks of text like a username
@@ -568,7 +568,7 @@ module ActionView
# # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset>
def field_set_tag(legend = nil, options = nil, &block)
output = tag(:fieldset, options, true)
- output.safe_concat(content_tag(:legend, legend)) unless legend.blank?
+ output.safe_concat(content_tag("legend".freeze, legend)) unless legend.blank?
output.concat(capture(&block)) if block_given?
output.safe_concat("</fieldset>")
end
diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb
index e237a32cb7..ed7e882c94 100644
--- a/actionview/lib/action_view/helpers/javascript_helper.rb
+++ b/actionview/lib/action_view/helpers/javascript_helper.rb
@@ -47,8 +47,8 @@ module ActionView
# tag.
#
# javascript_tag "alert('All is good')", defer: 'defer'
- #
- # Returns:
+ #
+ # Returns:
# <script defer="defer">
# //<![CDATA[
# alert('All is good')
@@ -70,7 +70,7 @@ module ActionView
content_or_options_with_block
end
- content_tag(:script, javascript_cdata_section(content), html_options)
+ content_tag("script".freeze, javascript_cdata_section(content), html_options)
end
def javascript_cdata_section(content) #:nodoc:
diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb
index a87c223a71..2562504896 100644
--- a/actionview/lib/action_view/helpers/tag_helper.rb
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -22,9 +22,10 @@ module ActionView
TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set
- PRE_CONTENT_STRINGS = {
- :textarea => "\n"
- }
+ PRE_CONTENT_STRINGS = Hash.new { "".freeze }
+ PRE_CONTENT_STRINGS[:textarea] = "\n"
+ PRE_CONTENT_STRINGS["textarea"] = "\n"
+
# Returns an empty HTML tag of type +name+ which by default is XHTML
# compliant. Set +open+ to true to create an open tag compatible
@@ -143,24 +144,30 @@ module ActionView
def content_tag_string(name, content, options, escape = true)
tag_options = tag_options(options, escape) if options
content = ERB::Util.unwrapped_html_escape(content) if escape
- "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe
+ "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
end
def tag_options(options, escape = true)
return if options.blank?
- attrs = []
+ output = ""
+ sep = " ".freeze
options.each_pair do |key, value|
if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
value.each_pair do |k, v|
- attrs << prefix_tag_option(key, k, v, escape)
+ output << sep
+ output << prefix_tag_option(key, k, v, escape)
end
elsif BOOLEAN_ATTRIBUTES.include?(key)
- attrs << boolean_tag_option(key) if value
+ if value
+ output << sep
+ output << boolean_tag_option(key)
+ end
elsif !value.nil?
- attrs << tag_option(key, value, escape)
+ output << sep
+ output << tag_option(key, value, escape)
end
end
- " #{attrs * ' '}" unless attrs.empty?
+ output unless output.empty?
end
def prefix_tag_option(prefix, key, value, escape)
@@ -177,7 +184,7 @@ module ActionView
def tag_option(key, value, escape)
if value.is_a?(Array)
- value = escape ? safe_join(value, " ") : value.join(" ")
+ value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze)
else
value = escape ? ERB::Util.unwrapped_html_escape(value) : value
end
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index 7d92651183..d676a0a931 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -184,9 +184,9 @@ module ActionView
html_options = convert_options_to_data_attributes(options, html_options)
url = url_for(options)
- html_options['href'] ||= url
+ html_options["href".freeze] ||= url
- content_tag(:a, name || url, html_options, &block)
+ content_tag("a".freeze, name || url, html_options, &block)
end
# Generates a form containing a single button that submits to the URL created
@@ -471,7 +471,7 @@ module ActionView
encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@")
html_options["href"] = "mailto:#{encoded_email_address}#{extras}"
- content_tag(:a, name || email_address, html_options, &block)
+ content_tag("a".freeze, name || email_address, html_options, &block)
end
# True if the current request URI was generated by the given +options+.
diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb
index 91ee2ea8f5..7a88f6bc50 100644
--- a/actionview/lib/action_view/path_set.rb
+++ b/actionview/lib/action_view/path_set.rb
@@ -61,6 +61,15 @@ module ActionView #:nodoc:
find_all(path, prefixes, *args).any?
end
+ def find_all_with_query(query) # :nodoc:
+ paths.each do |resolver|
+ templates = resolver.find_all_with_query(query)
+ return templates unless templates.empty?
+ end
+
+ []
+ end
+
private
def typecast(paths)
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 780fdabbd1..1f9e960488 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -520,7 +520,7 @@ module ActionView
def retrieve_variable(path, as)
variable = as || begin
- base = path[-1] == "/" ? "" : File.basename(path)
+ base = path[-1] == "/".freeze ? "".freeze : File.basename(path)
raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/
$1.to_sym
end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 1ce9f94b13..e232808dcb 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -154,7 +154,7 @@ module ActionView
# we use a bang in this instrumentation because you don't want to
# consume this in production. This is only slow if it's being listened to.
def render(view, locals, buffer=nil, &block)
- instrument("!render_template") do
+ instrument("!render_template".freeze) do
compile!(view)
view.send(method_name, locals, buffer, &block)
end
@@ -348,7 +348,12 @@ module ActionView
def instrument(action, &block)
payload = { virtual_path: @virtual_path, identifier: @identifier }
- ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block)
+ case action
+ when "!render_template".freeze
+ ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, payload, &block)
+ else
+ ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block)
+ end
end
EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index e7208cce49..28967f40a6 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -52,6 +52,7 @@ module ActionView
def initialize
@data = SmallCache.new(&KEY_BLOCK)
+ @query_cache = SmallCache.new
end
# Cache the templates returned by the block
@@ -70,8 +71,17 @@ module ActionView
end
end
+ def cache_query(query) # :nodoc:
+ if Resolver.caching?
+ @query_cache[query] ||= canonical_no_templates(yield)
+ else
+ yield
+ end
+ end
+
def clear
@data.clear
+ @query_cache.clear
end
private
@@ -116,6 +126,10 @@ module ActionView
end
end
+ def find_all_with_query(query) # :nodoc:
+ @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
+ end
+
private
delegate :caching?, to: :class
diff --git a/actionview/test/fixtures/digestor/events/_completed.html.erb b/actionview/test/fixtures/digestor/events/_completed.html.erb
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionview/test/fixtures/digestor/events/_completed.html.erb
diff --git a/actionview/test/fixtures/digestor/events/index.html.erb b/actionview/test/fixtures/digestor/events/index.html.erb
new file mode 100644
index 0000000000..bc45e41bcb
--- /dev/null
+++ b/actionview/test/fixtures/digestor/events/index.html.erb
@@ -0,0 +1 @@
+<% # Template Dependency: events/* %> \ No newline at end of file
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index 24bc76cbbb..dde757b5a2 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -1,5 +1,6 @@
require 'abstract_unit'
require 'fileutils'
+require 'action_view/dependency_tracker'
class FixtureTemplate
attr_reader :source, :handler
@@ -15,12 +16,13 @@ end
class FixtureFinder
FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor"
- attr_reader :details
+ attr_reader :details, :view_paths
attr_accessor :formats
attr_accessor :variants
def initialize
@details = {}
+ @view_paths = ActionView::PathSet.new(['digestor'])
@formats = []
@variants = []
end
@@ -75,6 +77,34 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
+ def test_explicit_dependency_wildcard
+ assert_digest_difference("events/index") do
+ change_template("events/_completed")
+ end
+ end
+
+ def test_explicit_dependency_wildcard_picks_up_added_file
+ old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false
+
+ assert_digest_difference("events/index") do
+ add_template("events/_uncompleted")
+ end
+ ensure
+ remove_template("events/_uncompleted")
+ ActionView::Resolver.caching = old_caching
+ end
+
+ def test_explicit_dependency_wildcard_picks_up_removed_file
+ old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false
+ add_template("events/_subscribers_changed")
+
+ assert_digest_difference("events/index") do
+ remove_template("events/_subscribers_changed")
+ end
+ ensure
+ ActionView::Resolver.caching = old_caching
+ end
+
def test_second_level_dependency
assert_digest_difference("messages/show") do
change_template("comments/_comments")
@@ -319,4 +349,9 @@ class TemplateDigestorTest < ActionView::TestCase
f.write "\nTHIS WAS CHANGED!"
end
end
+ alias_method :add_template, :change_template
+
+ def remove_template(template_name)
+ File.delete("digestor/#{template_name}.html.erb")
+ end
end
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index c523723fc4..1bd97eed38 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,3 +1,10 @@
+* Include I18n.locale into job serialization/deserialization and use it around
+ `perform`.
+
+ Fixes #20799.
+
+ *Johannes Opper*
+
* Allow `DelayedJob`, `Sidekiq`, `qu`, and `que` to report the job id back to
`ActiveJob::Base` as `provider_job_id`.
diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb
index fd49b3fda5..5d7c4cfb91 100644
--- a/activejob/lib/active_job/base.rb
+++ b/activejob/lib/active_job/base.rb
@@ -5,6 +5,7 @@ require 'active_job/enqueuing'
require 'active_job/execution'
require 'active_job/callbacks'
require 'active_job/logging'
+require 'active_job/translation'
module ActiveJob #:nodoc:
# = Active Job
@@ -60,6 +61,7 @@ module ActiveJob #:nodoc:
include Execution
include Callbacks
include Logging
+ include Translation
ActiveSupport.run_load_hooks(:active_job, self)
end
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
index 0528572cd0..eac7279309 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -20,6 +20,9 @@ module ActiveJob
# ID optionally provided by adapter
attr_accessor :provider_job_id
+
+ # I18n.locale to be used during the job.
+ attr_accessor :locale
end
# These methods will be included into any Active Job object, adding
@@ -68,7 +71,8 @@ module ActiveJob
'job_class' => self.class.name,
'job_id' => job_id,
'queue_name' => queue_name,
- 'arguments' => serialize_arguments(arguments)
+ 'arguments' => serialize_arguments(arguments),
+ 'locale' => I18n.locale
}
end
@@ -96,6 +100,7 @@ module ActiveJob
self.job_id = job_data['job_id']
self.queue_name = job_data['queue_name']
self.serialized_arguments = job_data['arguments']
+ self.locale = job_data['locale'] || I18n.locale
end
private
diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb
index 1335e3236e..e8ceabaeba 100644
--- a/activejob/lib/active_job/queue_adapters.rb
+++ b/activejob/lib/active_job/queue_adapters.rb
@@ -15,18 +15,18 @@ module ActiveJob
#
# === Backends Features
#
- # | | Async | Queues | Delayed | Priorities | Timeout | Retries |
- # |-------------------|-------|--------|-----------|------------|---------|---------|
- # | Backburner | Yes | Yes | Yes | Yes | Job | Global |
- # | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
- # | Qu | Yes | Yes | No | No | No | Global |
- # | Que | Yes | Yes | Yes | Job | No | Job |
- # | queue_classic | Yes | Yes | No* | No | No | No |
- # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
- # | Sidekiq | Yes | Yes | Yes | Queue | No | Job |
- # | Sneakers | Yes | Yes | No | Queue | Queue | No |
- # | Sucker Punch | Yes | Yes | No | No | No | No |
- # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A |
+ # | | Async | Queues | Delayed | Priorities | Timeout | Retries |
+ # |-------------------|-------|--------|------------|------------|---------|---------|
+ # | Backburner | Yes | Yes | Yes | Yes | Job | Global |
+ # | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
+ # | Qu | Yes | Yes | No | No | No | Global |
+ # | Que | Yes | Yes | Yes | Job | No | Job |
+ # | queue_classic | Yes | Yes | Yes* | No | No | No |
+ # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
+ # | Sidekiq | Yes | Yes | Yes | Queue | No | Job |
+ # | Sneakers | Yes | Yes | No | Queue | Queue | No |
+ # | Sucker Punch | Yes | Yes | No | No | No | No |
+ # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A |
#
# ==== Async
#
@@ -50,9 +50,8 @@ module ActiveJob
# N/A: The adapter does not support queueing.
#
# NOTE:
- # queue_classic does not support job scheduling.
- # However, you can use the queue_classic-later gem.
- # See the documentation for ActiveJob::QueueAdapters::QueueClassicAdapter.
+ # queue_classic supports job scheduling since version 3.1.
+ # For older versions you can use the queue_classic-later gem.
#
# ==== Priorities
#
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
index 9b307e8dc8..e71a3b4f85 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -7,7 +7,7 @@ module ActiveJob
extend ActiveSupport::Concern
included do
- def before_setup
+ def before_setup # :nodoc:
test_adapter = ActiveJob::QueueAdapters::TestAdapter.new
@old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass|
@@ -24,7 +24,7 @@ module ActiveJob
super
end
- def after_teardown
+ def after_teardown # :nodoc:
super
@old_queue_adapters.each do |(klass, adapter)|
klass.queue_adapter = adapter
diff --git a/activejob/lib/active_job/translation.rb b/activejob/lib/active_job/translation.rb
new file mode 100644
index 0000000000..67e4cf4ab9
--- /dev/null
+++ b/activejob/lib/active_job/translation.rb
@@ -0,0 +1,11 @@
+module ActiveJob
+ module Translation #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ around_perform do |job, block, _|
+ I18n.with_locale(job.locale, &block)
+ end
+ end
+ end
+end
diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb
index db22783030..229517774e 100644
--- a/activejob/test/cases/job_serialization_test.rb
+++ b/activejob/test/cases/job_serialization_test.rb
@@ -1,5 +1,6 @@
require 'helper'
require 'jobs/gid_job'
+require 'jobs/hello_job'
require 'models/person'
class JobSerializationTest < ActiveSupport::TestCase
@@ -12,4 +13,20 @@ class JobSerializationTest < ActiveSupport::TestCase
GidJob.perform_later @person
assert_equal "Person with ID: 5", JobBuffer.last_value
end
+
+ test 'serialize includes current locale' do
+ assert_equal :en, HelloJob.new.serialize['locale']
+ end
+
+ test 'deserialize sets locale' do
+ job = HelloJob.new
+ job.deserialize 'locale' => :es
+ assert_equal :es, job.locale
+ end
+
+ test 'deserialize sets default locale' do
+ job = HelloJob.new
+ job.deserialize({})
+ assert_equal :en, job.locale
+ end
end
diff --git a/activejob/test/cases/translation_test.rb b/activejob/test/cases/translation_test.rb
new file mode 100644
index 0000000000..d5e3aaf9e3
--- /dev/null
+++ b/activejob/test/cases/translation_test.rb
@@ -0,0 +1,20 @@
+require 'helper'
+require 'jobs/translated_hello_job'
+
+class TranslationTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ I18n.available_locales = [:en, :de]
+ @job = TranslatedHelloJob.new('Johannes')
+ end
+
+ teardown do
+ I18n.available_locales = [:en]
+ end
+
+ test 'it performs the job in the given locale' do
+ @job.locale = :de
+ @job.perform_now
+ assert_equal "Johannes says Guten Tag", JobBuffer.last_value
+ end
+end
diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb
index d345092dee..e1634972d1 100644
--- a/activejob/test/integration/queuing_test.rb
+++ b/activejob/test/integration/queuing_test.rb
@@ -68,4 +68,21 @@ class QueuingTest < ActiveSupport::TestCase
refute delayed_test_job.provider_job_id.nil?,
'Provider job id should by set for delayed jobs by provider'
end
+
+ test 'current locale is kept while running perform_later' do
+ skip if adapter_is?(:inline)
+
+ begin
+ I18n.available_locales = [:en, :de]
+ I18n.locale = :de
+
+ TestJob.perform_later @id
+ wait_for_jobs_to_finish_for(5.seconds)
+ assert job_executed
+ assert_equal 'de', job_output
+ ensure
+ I18n.available_locales = [:en]
+ I18n.locale = :en
+ end
+ end
end
diff --git a/activejob/test/jobs/translated_hello_job.rb b/activejob/test/jobs/translated_hello_job.rb
new file mode 100644
index 0000000000..9657cd3f54
--- /dev/null
+++ b/activejob/test/jobs/translated_hello_job.rb
@@ -0,0 +1,10 @@
+require_relative '../support/job_buffer'
+
+class TranslatedHelloJob < ActiveJob::Base
+ def perform(greeter = "David")
+ translations = { en: 'Hello', de: 'Guten Tag' }
+ hello = translations[I18n.locale]
+
+ JobBuffer.add("#{greeter} says #{hello}")
+ end
+end
diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb
index 09a68738ad..4ffdb8cffa 100644
--- a/activejob/test/support/integration/dummy_app_template.rb
+++ b/activejob/test/support/integration/dummy_app_template.rb
@@ -8,13 +8,17 @@ require "#{File.expand_path("../jobs_manager.rb", __FILE__)}"
JobsManager.current_manager.setup
CODE
+initializer 'i18n.rb', <<-CODE
+I18n.available_locales = [:en, :de]
+CODE
+
file 'app/jobs/test_job.rb', <<-CODE
class TestJob < ActiveJob::Base
queue_as :integration_tests
def perform(x)
File.open(Rails.root.join("tmp/\#{x}"), "w+") do |f|
- f.write x
+ f.write I18n.locale
end
end
end
diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb
index 7e87ede275..39aee6d407 100644
--- a/activejob/test/support/integration/test_case_helpers.rb
+++ b/activejob/test/support/integration/test_case_helpers.rb
@@ -45,4 +45,8 @@ module TestCaseHelpers
def job_executed
Dummy::Application.root.join("tmp/#{@id}").exist?
end
+
+ def job_output
+ File.read Dummy::Application.root.join("tmp/#{@id}")
+ end
end
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index ce9a782f73..09d7226b5a 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -101,7 +101,7 @@ class I18nValidationTest < ActiveModel::TestCase
Person.validates_length_of :title, validation_options.merge(within: 3..5)
@person.title = 'this title is too long'
call = [:title, :too_long, generate_message_options.merge(count: 5)]
- assert_called_with(@person.errors, :generate_message, ) do
+ assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
end
end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 4cf54ffeb1..5f273d53f2 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,8 +1,18 @@
+* Add `cache_key` to ActiveRecord::Relation.
+
+ Example:
+
+ @users = User.where("name like ?", "%Alberto%")
+ @users.cache_key
+ => "/users/query-5942b155a43b139f2471b872ac54251f-3-20150714212107656125000"
+
+ *Alberto Fernández-Capel*
+
* Properly allow uniqueness validations on primary keys.
Fixes #20966.
- *Sean Griffin & presskey*
+ *Sean Griffin*, *presskey*
* Don't raise an error if an association failed to destroy when `destroy` was
called on the parent (as opposed to `destroy!`).
@@ -11,10 +21,11 @@
*Sean Griffin*
-* ActiveRecord::RecordNotFound modified to store model name, primary_key and
+* `ActiveRecord::RecordNotFound` modified to store model name, primary_key and
id of the caller model. It allows the catcher of this exception to make
- a better decision to what to do with it. For example consider this simple
- example:
+ a better decision to what to do with it.
+
+ Example:
class SomeAbstractController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :redirect_to_404
@@ -64,12 +75,12 @@
*Sean Griffin*
-* Fix a bug where counter_cache doesn't always work with polymorphic
+* Fix a bug where counter_cache doesn't always work with polymorphic
relations.
Fixes #16407.
- *Stefan Kanev & Sean Griffin*
+ *Stefan Kanev*, *Sean Griffin*
* Ensure that cyclic associations with autosave don't cause duplicate errors
to be added to the parent record.
@@ -209,14 +220,12 @@
* Do not set `sql_mode` if `strict: :default` is specified.
- ```
- # database.yml
+ # config/database.yml
production:
adapter: mysql2
database: foo_prod
user: foo
strict: :default
- ```
*Ryuta Kamizono*
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index f5cf92db64..264f869c68 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -53,6 +53,7 @@ module ActiveRecord
autoload :Persistence
autoload :QueryCache
autoload :Querying
+ autoload :CollectionCacheKey
autoload :ReadonlyAttributes
autoload :RecordInvalid, 'active_record/validations'
autoload :Reflection
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 82cb3fed59..a830b0e0e4 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -61,12 +61,18 @@ module ActiveRecord
end
end
- class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
+ class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
def initialize(owner, reflection)
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
end
end
+ class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
+ end
+
+ class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
+ end
+
class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
def initialize(owner, reflection)
super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
@@ -79,12 +85,18 @@ module ActiveRecord
end
end
- class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
+ class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
def initialize(owner, reflection)
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
end
end
+ class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
+ end
+
+ class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
+ end
+
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index 55ee9f04e0..d0ec3e8015 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -76,13 +76,21 @@ module ActiveRecord
def ensure_mutable
unless source_reflection.belongs_to?
- raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ if reflection.has_one?
+ raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ else
+ raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ end
end
end
def ensure_not_nested
if reflection.nested?
- raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ if reflection.has_one?
+ raise HasOneThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ else
+ raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index abe1d465a5..7fb899c242 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -230,7 +230,15 @@ module ActiveRecord
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
return false unless super
- name = name.to_s
+
+ case name
+ when :to_partial_path
+ name = "to_partial_path".freeze
+ when :to_model
+ name = "to_model".freeze
+ else
+ name = name.to_s
+ end
# If the result is true then check for the select case.
# For queries selecting a subset of columns, return false for unselected columns.
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index c918e88590..55a7e053bc 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -280,6 +280,7 @@ module ActiveRecord #:nodoc:
extend Explain
extend Enum
extend Delegation::DelegateCache
+ extend CollectionCacheKey
include Core
include Persistence
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
new file mode 100644
index 0000000000..3c4ca3d116
--- /dev/null
+++ b/activerecord/lib/active_record/collection_cache_key.rb
@@ -0,0 +1,31 @@
+module ActiveRecord
+ module CollectionCacheKey
+
+ def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
+ query_signature = Digest::MD5.hexdigest(collection.to_sql)
+ key = "#{collection.model_name.cache_key}/query-#{query_signature}"
+
+ if collection.loaded?
+ size = collection.size
+ timestamp = collection.max_by(&timestamp_column).public_send(timestamp_column)
+ else
+ column_type = type_for_attribute(timestamp_column.to_s)
+ column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}"
+
+ query = collection
+ .select("COUNT(*) AS size", "MAX(#{column}) AS timestamp")
+ .unscope(:order)
+ result = connection.select_one(query)
+
+ size = result["size"]
+ timestamp = column_type.deserialize(result["timestamp"])
+ end
+
+ if timestamp
+ "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
+ else
+ "#{key}-#{size}"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index e97e82f056..b7db57c9fe 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -254,7 +254,7 @@ module ActiveRecord
end
def full_version
- @full_version ||= @connection.info[:version]
+ @full_version ||= @connection.server_info[:version]
end
def set_field_encoding field_name
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index d062dd9e34..f1dc56df63 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -827,12 +827,12 @@ module ActiveRecord
module TestFixtures
extend ActiveSupport::Concern
- def before_setup
+ def before_setup # :nodoc:
setup_fixtures
super
end
- def after_teardown
+ def after_teardown # :nodoc:
super
teardown_fixtures
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 4cfda302ea..b7b508d853 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -716,7 +716,9 @@ module ActiveRecord
end
end
- def table_name_options(config = ActiveRecord::Base)
+ # Builds a hash for use in ActiveRecord::Migration#proper_table_name using
+ # the Active Record object's table_name prefix and suffix
+ def table_name_options(config = ActiveRecord::Base) #:nodoc:
{
table_name_prefix: config.table_name_prefix,
table_name_suffix: config.table_name_suffix
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index e4df122af6..3ed04dee3b 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -298,6 +298,32 @@ module ActiveRecord
limit_value ? to_a.many? : size > 1
end
+ # Returns a cache key that can be used to identify the records fetched by
+ # this query. The cache key is built with a fingerprint of the sql query,
+ # the number of records matched by the query and a timestamp of the last
+ # updated record. When a new record comes to match the query, or any of
+ # the existing records is updated or deleted, the cache key changes.
+ #
+ # Product.where("name like ?", "%Cosmic Encounter%").cache_key
+ # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
+ #
+ # If the collection is loaded, the method will iterate through the records
+ # to generate the timestamp, otherwise it will trigger one SQL query like:
+ #
+ # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
+ #
+ # You can also pass a custom timestamp column to fetch the timestamp of the
+ # last updated record.
+ #
+ # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at)
+ #
+ # You can customize the strategy to generate the key on a per model basis
+ # overriding ActiveRecord::Base#collection_cache_key.
+ def cache_key(timestamp_column = :updated_at)
+ @cache_keys ||= {}
+ @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column)
+ end
+
# Scope all queries to the current scope.
#
# Comment.where(post_id: 1).scoping do
diff --git a/activerecord/test/cases/adapters/mysql2/quoting_test.rb b/activerecord/test/cases/adapters/mysql2/quoting_test.rb
index a49cbba4b4..92221e61bf 100644
--- a/activerecord/test/cases/adapters/mysql2/quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/quoting_test.rb
@@ -7,14 +7,14 @@ class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase
test 'quoted date precision for gte 5.6.4' do
@connection.stubs(:full_version).returns('5.6.4')
- @connection.remove_instance_variable(:@version)
+ @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
t = Time.now.change(usec: 1)
assert_match(/\.000001\z/, @connection.quoted_date(t))
end
test 'quoted date precision for lt 5.6.4' do
@connection.stubs(:full_version).returns('5.6.3')
- @connection.remove_instance_variable(:@version)
+ @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
t = Time.now.change(usec: 1)
refute_match(/\.000001\z/, @connection.quoted_date(t))
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index 97fd458994..2c8feb9152 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -352,4 +352,11 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_deprecated { member.club(true) }
end
+
+ def test_has_one_through_associations_are_mutable_unless_through_belongs_to
+ member_detail = MemberDetail.new(member: @member)
+ assert_raise(ActiveRecord::HasOneThroughCantAssociateThroughHasOneOrManyReflection) do
+ member_detail.membership = Membership.new
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 31b68c940e..b040485d99 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -495,7 +495,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
groucho = members(:groucho)
founding = member_types(:founding)
- assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ assert_raises(ActiveRecord::HasOneThroughNestedAssociationsAreReadonly) do
groucho.nested_member_type = founding
end
end
diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb
new file mode 100644
index 0000000000..724234d7f4
--- /dev/null
+++ b/activerecord/test/cases/collection_cache_key_test.rb
@@ -0,0 +1,70 @@
+require "cases/helper"
+require "models/computer"
+require "models/developer"
+require "models/project"
+require "models/topic"
+require "models/post"
+require "models/comment"
+
+module ActiveRecord
+ class CollectionCacheKeyTest < ActiveRecord::TestCase
+ fixtures :developers, :projects, :developers_projects, :topics, :comments, :posts
+
+ test "collection_cache_key on model" do
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, Developer.collection_cache_key)
+ end
+
+ test "cache_key for relation" do
+ developers = Developer.where(name: "David")
+ last_developer_timestamp = developers.order(updated_at: :desc).first.updated_at
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key)
+
+ /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key
+
+ assert_equal Digest::MD5.hexdigest(developers.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 "it triggers at most one query" do
+ developers = Developer.where(name: "David")
+
+ assert_queries(1) { developers.cache_key }
+ assert_queries(0) { developers.cache_key }
+ end
+
+ test "it doesn't trigger any query if the relation is already loaded" do
+ developers = Developer.where(name: "David").load
+ assert_queries(0) { developers.cache_key }
+ end
+
+ test "relation cache_key changes when the sql query changes" do
+ developers = Developer.where(name: "David")
+ other_relation = Developer.where(name: "David").where("1 = 1")
+
+ assert_not_equal developers.cache_key, other_relation.cache_key
+ end
+
+ test "cache_key for empty relation" do
+ developers = Developer.where(name: "Non Existent Developer")
+ assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key)
+ end
+
+ test "cache_key with custom timestamp column" do
+ topics = Topic.where("title like ?", "%Topic%")
+ last_topic_timestamp = topics(:fifth).written_on.utc.to_s(:nsec)
+ assert_match(last_topic_timestamp, topics.cache_key(:written_on))
+ end
+
+ test "cache_key with unknown timestamp column" do
+ topics = Topic.where("title like ?", "%Topic%")
+ assert_raises(ActiveRecord::StatementInvalid) { topics.cache_key(:published_at) }
+ end
+
+ test "collection proxy provides a cache_key" do
+ developers = projects(:active_record).developers
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key)
+ end
+ end
+end
diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb
index 9d253aa126..157130986c 100644
--- a/activerecord/test/models/member_detail.rb
+++ b/activerecord/test/models/member_detail.rb
@@ -1,7 +1,8 @@
class MemberDetail < ActiveRecord::Base
- belongs_to :member, :inverse_of => false
+ belongs_to :member, inverse_of: false
belongs_to :organization
- has_one :member_type, :through => :member
+ has_one :member_type, through: :member
+ has_one :membership, through: :member
- has_many :organization_member_details, :through => :organization, :source => :member_details
+ has_many :organization_member_details, through: :organization, source: :member_details
end
diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb
index 6e397abf51..2f6d38c1f6 100644
--- a/activesupport/lib/active_support/core_ext/hash/except.rb
+++ b/activesupport/lib/active_support/core_ext/hash/except.rb
@@ -1,8 +1,9 @@
class Hash
- # Returns a hash that includes everything but the given keys.
- # hash = { a: true, b: false, c: nil}
- # hash.except(:c) # => { a: true, b: false}
- # hash # => { a: true, b: false, c: nil}
+ # Returns a hash that includes everything except given keys.
+ # hash = { a: true, b: false, c: nil }
+ # hash.except(:c) # => { a: true, b: false }
+ # hash.except(:a, :b) # => { c: nil }
+ # hash # => { a: true, b: false, c: nil }
#
# This is useful for limiting a set of parameters to everything but a few known toggles:
# @person.update(params[:person].except(:admin))
@@ -10,10 +11,10 @@ class Hash
dup.except!(*keys)
end
- # Replaces the hash without the given keys.
- # hash = { a: true, b: false, c: nil}
- # hash.except!(:c) # => { a: true, b: false}
- # hash # => { a: true, b: false }
+ # Removes the given keys from hash and returns it.
+ # hash = { a: true, b: false, c: nil }
+ # hash.except!(:c) # => { a: true, b: false }
+ # hash # => { a: true, b: false }
def except!(*keys)
keys.each { |key| delete(key) }
self
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index 6fc0cb53f0..befa5aee21 100644
--- a/activesupport/lib/active_support/core_ext/object/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb
@@ -81,7 +81,7 @@ class BigDecimal
# BigDecimals are duplicable:
#
# BigDecimal.new("1.2").duplicable? # => true
- # BigDecimal.new("1.2").dup # => #<BigDecimal:7f9d698eee10,'0.12E1',18(18)>
+ # BigDecimal.new("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)>
def duplicable?
true
end
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index 97f9720b2b..0d5e02cd77 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -164,7 +164,7 @@ class String
#
# <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
- def parameterize(sep = '-')
+ def parameterize(sep = '-'.freeze)
ActiveSupport::Inflector.parameterize(self, sep)
end
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index c554501893..96156deebb 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -108,6 +108,12 @@ class Time
# takes a hash with any of these keys: <tt>:years</tt>, <tt>:months</tt>,
# <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, <tt>:minutes</tt>,
# <tt>:seconds</tt>.
+ #
+ # Time.new(2015, 8, 1, 14, 35, 0).advance(seconds: 1) # => 2015-08-01 14:35:01 -0700
+ # Time.new(2015, 8, 1, 14, 35, 0).advance(minutes: 1) # => 2015-08-01 14:36:00 -0700
+ # Time.new(2015, 8, 1, 14, 35, 0).advance(hours: 1) # => 2015-08-01 15:35:00 -0700
+ # Time.new(2015, 8, 1, 14, 35, 0).advance(days: 1) # => 2015-08-02 14:35:00 -0700
+ # Time.new(2015, 8, 1, 14, 35, 0).advance(weeks: 1) # => 2015-08-08 14:35:00 -0700
def advance(options)
unless options[:weeks].nil?
options[:weeks], partial_weeks = options[:weeks].divmod(1)
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index 486838bd15..8ac1820776 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -27,6 +27,37 @@ module ActiveSupport
class Inflections
@__instance__ = ThreadSafe::Cache.new
+ class Uncountables < Array
+ def initialize
+ @regex_array = []
+ super
+ end
+
+ def delete(entry)
+ super entry
+ @regex_array.delete(to_regex(entry))
+ end
+
+ def <<(*word)
+ add(word)
+ end
+
+ def add(words)
+ self.concat(words.flatten.map(&:downcase))
+ @regex_array += self.map {|word| to_regex(word) }
+ self
+ end
+
+ def uncountable?(str)
+ @regex_array.detect {|regex| regex.match(str) }
+ end
+
+ private
+ def to_regex(string)
+ /\b#{::Regexp.escape(string)}\Z/i
+ end
+ end
+
def self.instance(locale = :en)
@__instance__[locale] ||= new
end
@@ -34,7 +65,7 @@ module ActiveSupport
attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex
def initialize
- @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], [], [], {}, /(?=a)b/
+ @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], Uncountables.new, [], {}, /(?=a)b/
end
# Private, for the test suite.
@@ -160,7 +191,7 @@ module ActiveSupport
# uncountable 'money', 'information'
# uncountable %w( money information rice )
def uncountable(*words)
- @uncountables += words.flatten.map(&:downcase)
+ @uncountables.add(words)
end
# Specifies a humanized form of a string by a regular expression rule or
@@ -185,7 +216,7 @@ module ActiveSupport
def clear(scope = :all)
case scope
when :all
- @plurals, @singulars, @uncountables, @humans = [], [], [], []
+ @plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, []
else
instance_variable_set "@#{scope}", []
end
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index 60ef249e37..899ba70af6 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -354,7 +354,7 @@ module ActiveSupport
# const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
# const_regexp("::") # => "::"
def const_regexp(camel_cased_word) #:nodoc:
- parts = camel_cased_word.split("::")
+ parts = camel_cased_word.split("::".freeze)
return Regexp.escape(camel_cased_word) if parts.blank?
@@ -372,7 +372,7 @@ module ActiveSupport
def apply_inflections(word, rules)
result = word.to_s.dup
- if word.empty? || inflections.uncountables.include?(result.downcase[/\b\w+\Z/])
+ if word.empty? || inflections.uncountables.uncountable?(result)
result
else
rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index 2c03956672..7b28eeb6e2 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -58,7 +58,7 @@ module ActiveSupport
# I18n.locale = :de
# transliterate('Jürgen')
# # => "Juergen"
- def transliterate(string, replacement = "?")
+ def transliterate(string, replacement = "?".freeze)
I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize(
ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c),
:replacement => replacement)
@@ -75,13 +75,21 @@ module ActiveSupport
# Turn unwanted chars into the separator
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep)
unless sep.nil? || sep.empty?
- re_sep = Regexp.escape(sep)
+ if sep == "-".freeze
+ re_duplicate_seperator = /-{2,}/
+ re_leading_trailing_separator = /^-|-$/i
+ else
+ re_sep = Regexp.escape(sep)
+ re_duplicate_seperator = /#{re_sep}{2,}/
+ re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
+ end
# No more than one of the separator in a row.
- parameterized_string.gsub!(/#{re_sep}{2,}/, sep)
+ parameterized_string.gsub!(re_duplicate_seperator, sep)
# Remove leading/trailing separator.
- parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, ''.freeze)
+ parameterized_string.gsub!(re_leading_trailing_separator, ''.freeze)
end
- parameterized_string.downcase
+ parameterized_string.downcase!
+ parameterized_string
end
end
end
diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb
index 75f353f62c..70ac4a4d5c 100644
--- a/activesupport/lib/active_support/log_subscriber/test_helper.rb
+++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb
@@ -33,7 +33,7 @@ module ActiveSupport
# you can collect them doing @logger.logged(level), where level is the level
# used in logging, like info, debug, warn and so on.
module TestHelper
- def setup
+ def setup # :nodoc:
@logger = MockLogger.new
@notifier = ActiveSupport::Notifications::Fanout.new
@@ -44,7 +44,7 @@ module ActiveSupport
ActiveSupport::Notifications.notifier = @notifier
end
- def teardown
+ def teardown # :nodoc:
set_logger(nil)
ActiveSupport::Notifications.notifier = @old_notifier
end
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index f4de4a80d9..4de7fa4e4b 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -273,7 +273,7 @@ module ActiveSupport
compose(reorder_characters(decompose(:compatibility, codepoints)))
else
raise ArgumentError, "#{form} is not a valid normalization variant", caller
- end.pack('U*')
+ end.pack('U*'.freeze)
end
def downcase(string)
diff --git a/guides/Rakefile b/guides/Rakefile
index 3c2099ac02..00577377d7 100644
--- a/guides/Rakefile
+++ b/guides/Rakefile
@@ -7,7 +7,7 @@ namespace :guides do
desc "Generate HTML guides"
task :html do
- ENV["WARN_BROKEN_LINKS"] = "1" # authors can't disable this
+ ENV["WARNINGS"] = "1" # authors can't disable this
ruby "rails_guides.rb"
end
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index c31b50fcfc..d3dd0e5bf2 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -326,7 +326,7 @@ key. The list of emails can be an array of email addresses or a single string
with the addresses separated by commas.
```ruby
-class AdminMailer < ActionMailer::Base
+class AdminMailer < ApplicationMailer
default to: Proc.new { Admin.pluck(:email) },
from: 'notification@example.com'
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index 98c6cbd540..db7eeed19a 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -15,7 +15,7 @@ After reading this guide, you will know:
What is Action View?
--------------------
-Action View and Action Controller are the two major components of Action Pack. In Rails, web requests are handled by Action Pack, which splits the work into a controller part (performing the logic) and a view part (rendering a template). Typically, Action Controller will be concerned with communicating with the database and performing CRUD actions where necessary. Action View is then responsible for compiling the response.
+In Rails, web requests are handled by [Action Controller](action_controller_overview.html) and Action View. Typically, Action Controller will be concerned with communicating with the database and performing CRUD actions where necessary. Action View is then responsible for compiling the response.
Action View templates are written using embedded Ruby in tags mingled with HTML. To avoid cluttering the templates with boilerplate code, a number of helper classes provide common behavior for forms, dates, and strings. It's also easy to add new helpers to your application as it evolves.
diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md
index dd545b56f5..cb25a4c5f3 100644
--- a/guides/source/active_job_basics.md
+++ b/guides/source/active_job_basics.md
@@ -280,6 +280,19 @@ UserMailer.welcome(@user).deliver_later
```
+Internationalization
+--------------------
+
+Each job uses the `I18n.locale` set when the job was created. Useful if you send
+emails asynchronously:
+
+```ruby
+I18n.locale = :eo
+
+UserMailer.welcome(@user).deliver_later # Email will be localized to Esparanto.
+```
+
+
GlobalID
--------
diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md
index ce605c912e..980dfe6953 100644
--- a/guides/source/active_record_migrations.md
+++ b/guides/source/active_record_migrations.md
@@ -357,8 +357,8 @@ will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table
### Creating a Join Table
-Migration method `create_join_table` creates an HABTM join table. A typical use
-would be:
+The migration method `create_join_table` creates an HABTM (has and belongs to
+many) join table. A typical use would be:
```ruby
create_join_table :products, :categories
@@ -367,23 +367,21 @@ create_join_table :products, :categories
which creates a `categories_products` table with two columns called
`category_id` and `product_id`. These columns have the option `:null` set to
`false` by default. This can be overridden by specifying the `:column_options`
-option.
+option:
```ruby
-create_join_table :products, :categories, column_options: {null: true}
+create_join_table :products, :categories, column_options: { null: true }
```
-will create the `product_id` and `category_id` with the `:null` option as
-`true`.
-
-You can pass the option `:table_name` when you want to customize the table
-name. For example:
+By default, the name of the join table comes from the union of the first two
+arguments provided to create_join_table, in alphabetical order.
+To customize the name of the table, provide a `:table_name` option:
```ruby
create_join_table :products, :categories, table_name: :categorization
```
-will create a `categorization` table.
+creates a `categorization` table.
`create_join_table` also accepts a block, which you can use to add indices
(which are not created by default) or additional columns:
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index 4a610e8458..fc7dd3a6c6 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -403,13 +403,13 @@ When using the asset pipeline, paths to assets must be re-written and
underscored in Ruby) for the following asset classes: image, font, video, audio,
JavaScript and stylesheet.
-* `image-url("rails.png")` becomes `url(/assets/rails.png)`
-* `image-path("rails.png")` becomes `"/assets/rails.png"`.
+* `url(/assets/rails.png)` becomes `image-url("rails.png")`
+* `"/assets/rails.png"` becomes `image-path("rails.png")` .
The more generic form can also be used:
-* `asset-url("rails.png")` becomes `url(/assets/rails.png)`
-* `asset-path("rails.png")` becomes `"/assets/rails.png"`
+* `url(/assets/rails.png)` becomes `asset-url("rails.png")`
+* `"/assets/rails.png"` becomes `asset-path("rails.png")`
#### JavaScript/CoffeeScript and ERB
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index c0fa3cfd04..1191f5edfe 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -622,6 +622,19 @@ end
We pass `id: false` to `create_table` because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a `has_and_belongs_to_many` association like mangled model IDs, or exceptions about conflicting IDs, chances are you forgot that bit.
+You can also use the method `create_join_table`
+
+```ruby
+class CreateAssembliesPartsJoinTable < ActiveRecord::Migration
+ def change
+ create_join_table :assemblies, :parts do |t|
+ t.index :assembly_id
+ t.index :part_id
+ end
+ end
+end
+```
+
### Controlling Association Scope
By default, associations look for objects only within the current module's scope. This can be important when you declare Active Record models within a module. For example:
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index 20f11c2bc2..9a56233e4a 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -3,12 +3,24 @@
Caching with Rails: An Overview
===============================
-This guide is an introduction to speeding up your Rails app with caching.
+This guide is an introduction to speeding up your Rails application with caching.
+
+Caching means to store content generated during the request-response cycle and
+to reuse it when responding to similar requests.
+
+Caching is often the most effective way to boost an application's performance.
+Through caching, web sites running on a single server with a single database
+can sustain a load of thousands of concurrent users.
+
+Rails provides a set of caching features out of the box. This guide will teach
+you the scope and purpose of each one of them. Master these techniques and your
+Rails applications can serve millions of views without exorbitant response times
+or server bills.
After reading this guide, you will know:
-* Page and action caching.
* Fragment and Russian doll caching.
+* How to manage the caching dependencies.
* Alternative cache stores.
* Conditional GET support.
@@ -32,7 +44,7 @@ config.action_controller.perform_caching = true
```
NOTE: Changing the value of `config.action_controller.perform_caching` will
-only have an effect on the caching provided by the Action Controller component.
+only have an effect on the caching provided by the Action Controller component.
For instance, it will not impact low-level caching, that we address
[below](#low-level-caching).
@@ -103,6 +115,30 @@ If you want to cache a fragment under certain conditions, you can use
<% end %>
```
+#### Collection caching
+
+The `render` helper can also cache individual templates rendered for a collection.
+It can even one up the previous example with `each` by reading all cache
+templates at once instead of one by one. This is done automatically if the template
+rendered by the collection includes a `cache` call. Take a collection that renders
+a `products/_product.html.erb` partial for each element:
+
+```ruby
+render products
+```
+
+If `products/_product.html.erb` starts with a `cache` call like so:
+
+```html+erb
+<% cache product do %>
+ <%= product.name %>
+<% end %>
+```
+
+All the cached templates from previous renders will be fetched at once with much
+greater speed. There's more info on how to make your templates [eligible for
+collection caching](http://api.rubyonrails.org/classes/ActionView/Template/Handlers/ERB.html#method-i-resource_cache_call_pattern).
+
### Russian Doll Caching
You may want to nest cached fragments inside other cached fragments. This is
@@ -152,6 +188,93 @@ With `touch` set to true, any action which changes `updated_at` for a game
record will also change it for the associated product, thereby expiring the
cache.
+### Managing dependencies
+
+In order to correctly invalidate the cache, you need to properly define the
+caching dependencies. Rails is clever enough to handle common cases so you don't
+have to specify anything. However, sometimes, when you're dealing with custom
+helpers for instance, you need to explicitly define them.
+
+#### Implicit dependencies
+
+Most template dependencies can be derived from calls to `render` in the template
+itself. Here are some examples of render calls that `ActionView::Digestor` knows
+how to decode:
+
+```ruby
+render partial: "comments/comment", collection: commentable.comments
+render "comments/comments"
+render 'comments/comments'
+render('comments/comments')
+
+render "header" => render("comments/header")
+
+render(@topic) => render("topics/topic")
+render(topics) => render("topics/topic")
+render(message.topics) => render("topics/topic")
+```
+
+On the other hand, some calls need to be changed to make caching work properly.
+For instance, if you're passing a custom collection, you'll need to change:
+
+```ruby
+render @project.documents.where(published: true)
+```
+
+to:
+
+```ruby
+render partial: "documents/document", collection: @project.documents.where(published: true)
+```
+
+#### Explicit dependencies
+
+Sometimes you'll have template dependencies that can't be derived at all. This
+is typically the case when rendering happens in helpers. Here's an example:
+
+```html+erb
+<%= render_sortable_todolists @project.todolists %>
+```
+
+You'll need to use a special comment format to call those out:
+
+```html+erb
+<%# Template Dependency: todolists/todolist %>
+<%= render_sortable_todolists @project.todolists %>
+```
+
+In some cases, like a single table inheritance setup, you might have a bunch of
+explicit dependencies. Instead of writing every template out, you can use a
+wildcard to match any template in a directory:
+
+```html+erb
+<%# Template Dependency: events/* %>
+<%= render_categorizable_events @person.events %>
+```
+
+As for collection caching, if the partial template doesn't start with a clean
+cache call, you can still benefit from collection caching by adding a special
+comment format anywhere in the template, like:
+
+```html+erb
+<%# Template Collection: notification %>
+<% my_helper_that_calls_cache(some_arg, notification) do %>
+ <%= notification.name %>
+<% end %>
+```
+
+#### External dependencies
+
+If you use a helper method, for example, inside a cached block and you then update
+that helper, you'll have to bump the cache as well. It doesn't really matter how
+you do it, but the md5 of the template file must change. One recommendation is to
+simply be explicit in a comment, like:
+
+```html+erb
+<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
+<%= some_helper_method(person) %>
+```
+
### Low-Level Caching
Sometimes you need to cache a particular value or query result instead of caching view fragments. Rails' caching mechanism works great for storing __any__ kind of information.
@@ -207,16 +330,17 @@ persistent fashion, you can with low level caching.
Cache Stores
------------
-Rails provides different stores for the cached data created by **action** and **fragment** caches.
-
-TIP: Page caches are always stored on disk.
+Rails provides different stores for the cached data (apart from SQL and page
+caching).
### Configuration
-You can set up your application's default cache store by calling `config.cache_store=` in the Application definition inside your `config/application.rb` file or in an Application.configure block in an environment specific configuration file (i.e. `config/environments/*.rb`). The first argument will be the cache store to use and the rest of the argument will be passed as arguments to the cache store constructor.
+You can set up your application's default cache store by setting the
+`config.cache_store` configuration option. Other parameters can be passed as
+arguments to the cache store's constructor:
```ruby
-config.cache_store = :memory_store
+config.cache_store = :memory_store, { size: 64.megabytes }
```
NOTE: Alternatively, you can call `ActionController::Base.cache_store` outside of a configuration block.
@@ -241,6 +365,19 @@ There are some common options used by all cache implementations. These can be pa
* `:race_condition_ttl` - This option is used in conjunction with the `:expires_in` option. It will prevent race conditions when cache entries expire by preventing multiple processes from simultaneously regenerating the same entry (also known as the dog pile effect). This option sets the number of seconds that an expired entry can be reused while a new value is being regenerated. It's a good practice to set this value if you use the `:expires_in` option.
+#### Custom Cache Stores
+
+You can create your own custom cache store by simply extending
+`ActiveSupport::Cache::Store` and implementing the appropriate methods. This way,
+you can swap in any number of caching technologies into your Rails application.
+
+To use a custom cache store, simply set the cache store to a new instance of your
+custom class.
+
+```ruby
+config.cache_store = MyCacheStore.new
+```
+
### ActiveSupport::Cache::MemoryStore
This cache store keeps entries in memory in the same Ruby process. The cache
@@ -292,36 +429,6 @@ The `write` and `fetch` methods on this cache accept two additional options that
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
```
-### ActiveSupport::Cache::EhcacheStore
-
-If you are using JRuby you can use Terracotta's Ehcache as the cache store for your application. Ehcache is an open source Java cache that also offers an enterprise version with increased scalability, management, and commercial support. You must first install the jruby-ehcache-rails3 gem (version 1.1.0 or later) to use this cache store.
-
-```ruby
-config.cache_store = :ehcache_store
-```
-
-When initializing the cache, you may use the `:ehcache_config` option to specify the Ehcache config file to use (where the default is "ehcache.xml" in your Rails config directory), and the :cache_name option to provide a custom name for your cache (the default is rails_cache).
-
-In addition to the standard `:expires_in` option, the `write` method on this cache can also accept the additional `:unless_exist` option, which will cause the cache store to use Ehcache's `putIfAbsent` method instead of `put`, and therefore will not overwrite an existing entry. Additionally, the `write` method supports all of the properties exposed by the [Ehcache Element class](http://ehcache.org/apidocs/net/sf/ehcache/Element.html) , including:
-
-| Property | Argument Type | Description |
-| --------------------------- | ------------------- | ----------------------------------------------------------- |
-| elementEvictionData | ElementEvictionData | Sets this element's eviction data instance. |
-| eternal | boolean | Sets whether the element is eternal. |
-| timeToIdle, tti | int | Sets time to idle |
-| timeToLive, ttl, expires_in | int | Sets time to Live |
-| version | long | Sets the version attribute of the ElementAttributes object. |
-
-These options are passed to the `write` method as Hash options using either camelCase or underscore notation, as in the following examples:
-
-```ruby
-Rails.cache.write('key', 'value', time_to_idle: 60.seconds, timeToLive: 600.seconds)
-caches_action :index, expires_in: 60.seconds, unless_exist: true
-```
-
-For more information about Ehcache, see [http://ehcache.org/](http://ehcache.org/) .
-For more information about Ehcache for JRuby and Rails, see [http://ehcache.org/documentation/jruby.html](http://ehcache.org/documentation/jruby.html)
-
### ActiveSupport::Cache::NullStore
This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache` but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss.
@@ -330,19 +437,13 @@ This cache store implementation is meant to be used only in development or test
config.cache_store = :null_store
```
-### Custom Cache Stores
-
-You can create your own custom cache store by simply extending `ActiveSupport::Cache::Store` and implementing the appropriate methods. In this way, you can swap in any number of caching technologies into your Rails application.
-
-To use a custom cache store, simply set the cache store to a new instance of the class.
-
-```ruby
-config.cache_store = MyCacheStore.new
-```
-
-### Cache Keys
+Cache Keys
+----------
-The keys used in a cache can be any object that responds to either `:cache_key` or `:to_param`. You can implement the `:cache_key` method on your classes if you need to generate custom keys. Active Record will generate keys based on the class name and record id.
+The keys used in a cache can be any object that responds to either `cache_key` or
+`to_param`. You can implement the `cache_key` method on your classes if you need
+to generate custom keys. Active Record will generate keys based on the class name
+and record id.
You can use Hashes and Arrays of values as cache keys.
@@ -351,7 +452,12 @@ You can use Hashes and Arrays of values as cache keys.
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
```
-The keys you use on `Rails.cache` will not be the same as those actually used with the storage engine. They may be modified with a namespace or altered to fit technology backend constraints. This means, for instance, that you can't save values with `Rails.cache` and then try to pull them out with the `memcache-client` gem. However, you also don't need to worry about exceeding the memcached size limit or violating syntax rules.
+The keys you use on `Rails.cache` will not be the same as those actually used with
+the storage engine. They may be modified with a namespace or altered to fit
+technology backend constraints. This means, for instance, that you can't save
+values with `Rails.cache` and then try to pull them out with the `dalli` gem.
+However, you also don't need to worry about exceeding the memcached size limit or
+violating syntax rules.
Conditional GET support
-----------------------
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 9c80e73c73..47c275609e 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -414,7 +414,7 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
`config.action_view` includes a small number of configuration settings:
-* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Record. The default is
+* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Model. The default is
```ruby
Proc.new do |html_tag, instance|
diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb
index 61ea0b44ef..1d995581fa 100644
--- a/guides/source/credits.html.erb
+++ b/guides/source/credits.html.erb
@@ -28,7 +28,7 @@ Ruby on Rails Guides: Credits
<h3 class="section">Rails Guides Authors</h3>
<%= author('Ryan Bigg', 'radar', 'radar.png') do %>
- Ryan Bigg works as the Community Manager at <a href="http://spreecommerce.com">Spree Commerce</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>.
+ Ryan Bigg works as a Rails developer at <a href="http://marketplacer.com">Marketplacer</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>.
<% end %>
<%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %>
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index 9145aee009..4473eba478 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -122,6 +122,10 @@
url: autoloading_and_reloading_constants.html
description: This guide documents how autoloading and reloading constants work.
-
+ name: "Caching with Rails: An Overview"
+ url: caching_with_rails.html
+ description: This guide is an introduction to speeding up your Rails application with caching.
+ -
name: Active Support Instrumentation
work_in_progress: true
url: active_support_instrumentation.html
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index 8f7d97844e..84a8d695cb 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -211,9 +211,8 @@ IMPORTANT: The search, telephone, date, time, color, datetime, datetime-local,
month, week, URL, email, number and range inputs are HTML5 controls.
If you require your app to have a consistent experience in older browsers,
you will need an HTML5 polyfill (provided by CSS and/or JavaScript).
-There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a couple of popular tools at the moment are
-[Modernizr](http://www.modernizr.com/) and [yepnope](http://yepnopejs.com/),
-which provide a simple way to add functionality based on the presence of
+There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a popular tool at the moment is
+[Modernizr](http://www.modernizr.com/), which provides a simple way to add functionality based on the presence of
detected HTML5 features.
TIP: If you're using password input fields (for any purpose), you might want to configure your application to prevent those parameters from being logged. You can learn about this in the [Security Guide](security.html#logging).
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 400383cfb5..dbbedc49ab 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -23,10 +23,12 @@ application from scratch. It does not assume that you have any prior experience
with Rails. However, to get the most out of it, you need to have some
prerequisites installed:
-* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 2.2.2 or newer.
-* Right version of [Development Kit](http://rubyinstaller.org/downloads/), if you are using Windows
-* The [RubyGems](https://rubygems.org) packaging system, which is installed with Ruby
- versions 1.9 and later. To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org).
+* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 2.2.2 or newer.
+* Right version of [Development Kit](http://rubyinstaller.org/downloads/), if you
+ are using Windows.
+* The [RubyGems](https://rubygems.org) packaging system, which is installed with
+ Ruby by default. To learn more about RubyGems, please read the
+ [RubyGems Guides](http://guides.rubygems.org).
* A working installation of the [SQLite3 Database](https://www.sqlite.org).
Rails is a web application framework running on the Ruby programming language.
diff --git a/guides/source/routing.md b/guides/source/routing.md
index 52f11f92bd..cf828462ce 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -615,6 +615,8 @@ get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
Rails would match `photos/12` to the `show` action of `PhotosController`, and set `params[:format]` to `"jpg"`.
+NOTE: You cannot override defaults via query parameters - this is for security reasons. The only defaults that can be overridden are dynamic segments via substitution in the URL path.
+
### Naming Routes
You can specify a name for any route using the `:as` option:
@@ -1087,6 +1089,20 @@ edit_videos GET /videos/:identifier/edit(.:format) videos#edit
Video.find_by(identifier: params[:identifier])
```
+You can override `ActiveRecord::Base#to_param` of a related
+model to constructe an URL.
+
+```ruby
+class Video < ActiveRecord::Base
+ def to_param # overridden
+ identifier
+ end
+end
+
+video = Video.find_by(identifier: "Roman-Holiday")
+edit_videos_path(video) # => "/videos/Roman-Holiday"
+```
+
Inspecting and Testing Routes
-----------------------------
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
index f3d3a83afc..4c996dd2d0 100644
--- a/guides/source/working_with_javascript_in_rails.md
+++ b/guides/source/working_with_javascript_in_rails.md
@@ -258,7 +258,7 @@ this generates
```html
<form action="/articles/1" class="button_to" data-remote="true" method="post">
- <div><input type="submit" value="An article"></div>
+ <input type="submit" value="An article" />
</form>
```
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 573b1f8f69..642fd90ae1 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,20 @@
+* Make enabling or disabling caching in development mode possible with
+ rake dev:cache.
+
+ Running rake dev:cache will create or remove tmp/caching-dev.txt. When this
+ file exists config.action_controller.perform_caching will be set to true in
+ config/environments/development.rb.
+
+ Additionally, a server can be started with either --dev-caching or
+ --no-dev-caching included to toggle caching on startup.
+
+ *Jussi Mertanen*, *Chuck Callebs*
+
+* Add a `--api` option in order to generate plugins that can be added
+ inside an API application.
+
+ *Robin Dupret*
+
* Fix `NoMethodError` when generating a scaffold inside a full engine.
*Yuji Yaginuma*
diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb
index 9a29ec21cf..618a09a5b3 100644
--- a/railties/lib/rails/application_controller.rb
+++ b/railties/lib/rails/application_controller.rb
@@ -6,7 +6,7 @@ class Rails::ApplicationController < ActionController::Base # :nodoc:
def require_local!
unless local_request?
- render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden
+ render html: '<p>For security purposes, this information is only available to local requests.</p>'.html_safe, status: :forbidden
end
end
diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb
index d1e445ac70..c8fb58ab05 100644
--- a/railties/lib/rails/commands/server.rb
+++ b/railties/lib/rails/commands/server.rb
@@ -34,6 +34,9 @@ module Rails
opts.on("-P", "--pid=pid", String,
"Specifies the PID file.",
"Default: tmp/pids/server.pid") { |v| options[:pid] = v }
+ opts.on("-C", "--[no-]dev-caching",
+ "Specifies whether to perform caching in development.",
+ "true or false") { |v| options[:caching] = v }
opts.separator ""
@@ -67,6 +70,7 @@ module Rails
print_boot_information
trap(:INT) { exit }
create_tmp_directories
+ setup_dev_caching
log_to_stdout if options[:log_stdout]
super
@@ -86,12 +90,23 @@ module Rails
DoNotReverseLookup: true,
environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup,
daemonize: false,
+ caching: false,
pid: File.expand_path("tmp/pids/server.pid")
})
end
private
+ def setup_dev_caching
+ return unless options[:environment] == "development"
+
+ if options[:caching] == false
+ delete_cache_file
+ elsif options[:caching]
+ create_cache_file
+ end
+ end
+
def print_boot_information
url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
@@ -101,6 +116,14 @@ module Rails
puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
end
+ def create_cache_file
+ FileUtils.touch("tmp/caching-dev.txt")
+ end
+
+ def delete_cache_file
+ FileUtils.rm("tmp/caching-dev.txt") if File.exist?("tmp/caching-dev.txt")
+ end
+
def create_tmp_directories
%w(cache pids sockets).each do |dir_to_make|
FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 1dede32dd4..07d0c270b7 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -519,17 +519,15 @@ module Rails
# Define the Rack API for this engine.
def call(env)
env.merge!(env_config)
- if env['SCRIPT_NAME']
- env[routes.env_key] = env['SCRIPT_NAME'].dup
- end
+ req = ActionDispatch::Request.new env
+ req.routes = routes
+ req.engine_script_name = req.script_name
app.call(env)
end
# Defines additional Rack env configuration that is added on each call.
def env_config
- @env_config ||= {
- 'action_dispatch.routes' => routes
- }
+ @env_config ||= {}
end
# Defines the routes for this engine. If a block is given to
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index 560a553789..b4356f71e0 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -92,11 +92,11 @@ module Rails
# file in <tt>config/environments</tt>.
#
# environment do
- # "config.autoload_paths += %W(#{config.root}/extras)"
+ # "config.action_controller.asset_host = 'cdn.provider.com'"
# end
#
# environment(nil, env: "development") do
- # "config.autoload_paths += %W(#{config.root}/extras)"
+ # "config.action_controller.asset_host = 'localhost:3000'"
# end
def environment(data=nil, options={})
sentinel = /class [a-z_:]+ < Rails::Application/i
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
index cb86978d4c..c88426ec06 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
@@ -5,7 +5,7 @@
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// compiled file.
+// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
index 0cdd2788d0..0ebd7fe829 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
@@ -7,7 +7,8 @@
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
- * files in this directory. It is generally better to create a new file per style scope.
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup
index a57b16444c..0c8b179827 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/setup
+++ b/railties/lib/rails/generators/rails/app/templates/bin/setup
@@ -15,7 +15,7 @@ chdir APP_ROOT do
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
- system!('bundle check') or system!('bundle install')
+ system('bundle check') or system!('bundle install')
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
index ecb5d4170f..34c60024a8 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
@@ -9,9 +9,19 @@ Rails.application.configure do
# Do not eager load code on boot.
config.eager_load = false
- # Show full error reports and disable caching.
+ # Show full error reports.
config.consider_all_requests_local = true
- config.action_controller.perform_caching = false
+
+ # Enable/disable caching. By default caching is disabled.
+ if Rails.root.join('tmp/caching-dev.txt').exist?
+ config.action_controller.perform_caching = true
+ config.static_cache_control = "public, max-age=172800"
+ config.cache_store = :memory_store
+ else
+ config.action_controller.perform_caching = false
+ config.cache_store = :null_store
+ end
+
<%- unless options.skip_action_mailer? -%>
# Don't care if the mailer can't send.
diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
index 66111004aa..0e1326ce4f 100644
--- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -17,15 +17,22 @@ module Rails
def app
if mountable?
- directory 'app'
- empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
+ if api?
+ directory 'app', exclude_pattern: %r{app/(views|helpers)}
+ else
+ directory 'app'
+ empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
+ end
elsif full?
empty_directory_with_keep_file 'app/models'
empty_directory_with_keep_file 'app/controllers'
- empty_directory_with_keep_file 'app/views'
- empty_directory_with_keep_file 'app/helpers'
empty_directory_with_keep_file 'app/mailers'
- empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
+
+ unless api?
+ empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
+ empty_directory_with_keep_file 'app/helpers'
+ empty_directory_with_keep_file 'app/views'
+ end
end
end
@@ -82,6 +89,7 @@ task default: :test
opts = (options || {}).slice(*PASSTHROUGH_OPTIONS)
opts[:force] = force
opts[:skip_bundle] = true
+ opts[:api] = options.api?
invoke Rails::Generators::AppGenerator,
[ File.expand_path(dummy_path, destination_root) ], opts
@@ -176,6 +184,9 @@ task default: :test
desc: "If creating plugin in application's directory " +
"skip adding entry to Gemfile"
+ class_option :api, type: :boolean, default: false,
+ desc: "Generate a smaller stack for API application plugins"
+
def initialize(*args)
@dummy_path = nil
super
@@ -210,15 +221,15 @@ task default: :test
end
def create_public_stylesheets_files
- build(:stylesheets)
+ build(:stylesheets) unless api?
end
def create_javascript_files
- build(:javascripts)
+ build(:javascripts) unless api?
end
def create_images_directory
- build(:images)
+ build(:images) unless api?
end
def create_bin_files
@@ -305,6 +316,10 @@ task default: :test
options[:skip_test].blank? || options[:dummy_path] != 'test/dummy'
end
+ def api?
+ options[:api]
+ end
+
def self.banner
"rails plugin new #{self.arguments.map(&:usage).join(' ')} [options]"
end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
index 7157e48c42..7fe4e5034d 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
@@ -1,5 +1,5 @@
<%= wrap_in_modules <<-rb.strip_heredoc
- class ApplicationController < ActionController::Base
+ class ApplicationController < ActionController::#{api? ? "API" : "Base"}
end
rb
%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb
index 17afd52177..8938770fc4 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb
@@ -1,6 +1,7 @@
<%= wrap_in_modules <<-rb.strip_heredoc
class Engine < ::Rails::Engine
#{mountable? ? ' isolate_namespace ' + camelized_modules : ' '}
+ #{api? ? " config.generators.api_only = true" : ' '}
end
rb
%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
index 8913b40f69..e54c6461cc 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
@@ -5,7 +5,7 @@
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// compiled file.
+// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css
index 0cdd2788d0..0ebd7fe829 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css
@@ -7,7 +7,8 @@
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
- * files in this directory. It is generally better to create a new file per style scope.
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb
index 41422a656c..78a857e0f1 100644
--- a/railties/lib/rails/mailers_controller.rb
+++ b/railties/lib/rails/mailers_controller.rb
@@ -26,7 +26,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
if part = find_part(part_type)
response.content_type = part_type
- render text: part.respond_to?(:decoded) ? part.decoded : part
+ render plain: part.respond_to?(:decoded) ? part.decoded : part
else
raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}"
end
diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb
index 2c3d278eca..c029dbcf82 100644
--- a/railties/lib/rails/tasks.rb
+++ b/railties/lib/rails/tasks.rb
@@ -3,6 +3,7 @@ require 'rake'
# Load Rails Rakefile extensions
%w(
annotations
+ dev
framework
initializers
log
diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake
new file mode 100644
index 0000000000..e949172d3f
--- /dev/null
+++ b/railties/lib/rails/tasks/dev.rake
@@ -0,0 +1,15 @@
+namespace :dev do
+ task :cache do
+ desc 'Toggle development mode caching on/off'
+
+ if File.exist? 'tmp/caching-dev.txt'
+ File.delete 'tmp/caching-dev.txt'
+ puts 'Development mode is no longer being cached.'
+ else
+ FileUtils.touch 'tmp/caching-dev.txt'
+ puts 'Development mode is now being cached.'
+ end
+
+ FileUtils.touch 'tmp/restart.txt'
+ end
+end
diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb
index 828039dc43..5cc1b5b219 100644
--- a/railties/lib/rails/test_help.rb
+++ b/railties/lib/rails/test_help.rb
@@ -28,14 +28,14 @@ if defined?(ActiveRecord::Base)
end
class ActionController::TestCase
- def before_setup
+ def before_setup # :nodoc:
@routes = Rails.application.routes
super
end
end
class ActionDispatch::IntegrationTest
- def before_setup
+ def before_setup # :nodoc:
@routes = Rails.application.routes
super
end
diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb
index ab71298509..5552779ef1 100644
--- a/railties/lib/rails/test_unit/minitest_plugin.rb
+++ b/railties/lib/rails/test_unit/minitest_plugin.rb
@@ -48,4 +48,5 @@ module Minitest
mattr_accessor(:run_with_rails_extension) { false }
end
+Minitest.load_plugins
Minitest.extensions << 'rails'
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index 6952472ff5..ba2c27d8e8 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -60,6 +60,7 @@ module ApplicationTests
test "assets do not require compressors until it is used" do
app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();"
add_to_env_config "production", "config.assets.compile = true"
+ add_to_env_config "production", "config.assets.precompile = []"
ENV["RAILS_ENV"] = "production"
require "#{app_path}/config/environment"
diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb
new file mode 100644
index 0000000000..28d8b22a37
--- /dev/null
+++ b/railties/test/application/rake/dev_test.rb
@@ -0,0 +1,35 @@
+require 'isolation/abstract_unit'
+
+module ApplicationTests
+ module RakeTests
+ class RakeDevTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ boot_rails
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test 'dev:cache creates file and outputs message' do
+ Dir.chdir(app_path) do
+ output = `rake dev:cache`
+ assert File.exist?('tmp/caching-dev.txt')
+ assert_match(/Development mode is now being cached/, output)
+ end
+ end
+
+ test 'dev:cache deletes file and outputs message' do
+ Dir.chdir(app_path) do
+ output = `rake dev:cache`
+ output = `rake dev:cache`
+ assert_not File.exist?('tmp/caching-dev.txt')
+ assert_match(/Development mode is no longer being cached/, output)
+ end
+ end
+ end
+ end
+end
diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb
index d6f5fee5c3..c87515f00f 100644
--- a/railties/test/application/rake/notes_test.rb
+++ b/railties/test/application/rake/notes_test.rb
@@ -114,6 +114,7 @@ module ApplicationTests
end
test 'register a new extension' do
+ add_to_config "config.assets.precompile = []"
add_to_config %q{ config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } }
app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss"
app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass"
diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb
index ba688f1e9e..7a063aeddf 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -44,6 +44,22 @@ class Rails::ServerTest < ActiveSupport::TestCase
end
end
+ def test_caching_without_option
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal nil, options[:caching]
+ end
+
+ def test_caching_with_option
+ args = ["--dev-caching"]
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:caching]
+
+ args = ["--no-dev-caching"]
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal false, options[:caching]
+ end
+
def test_log_stdout
with_rack_env nil do
with_rails_env nil do
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index d66b28117d..1898691806 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -545,6 +545,60 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_skipping_useless_folders_generation_for_api_engines
+ ['--full', '--mountable'].each do |option|
+ run_generator [destination_root, option, '--api']
+
+ assert_no_directory "app/assets"
+ assert_no_directory "app/helpers"
+ assert_no_directory "app/views"
+
+ FileUtils.rm_rf destination_root
+ end
+ end
+
+ def test_application_controller_parent_for_mountable_api_plugins
+ run_generator [destination_root, '--mountable', '--api']
+
+ assert_file "app/controllers/bukkits/application_controller.rb" do |content|
+ assert_match "ApplicationController < ActionController::API", content
+ end
+ end
+
+ def test_dummy_api_application_for_api_plugins
+ run_generator [destination_root, '--api']
+
+ assert_file "test/dummy/config/application.rb" do |content|
+ assert_match "config.api_only = true", content
+ end
+ end
+
+
+ def test_api_generators_configuration_for_api_engines
+ run_generator [destination_root, '--full', '--api']
+
+ assert_file "lib/bukkits/engine.rb" do |content|
+ assert_match "config.generators.api_only = true", content
+ end
+ end
+
+ def test_scaffold_generator_for_mountable_api_plugins
+ run_generator [destination_root, '--mountable', '--api']
+
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g scaffold article`
+ end
+
+ assert_file "app/models/bukkits/article.rb"
+ assert_file "app/controllers/bukkits/articles_controller.rb" do |content|
+ assert_match "only: [:show, :update, :destroy]", content
+ end
+
+ assert_no_directory "app/assets"
+ assert_no_directory "app/helpers"
+ assert_no_directory "app/views"
+ end
+
protected
def action(*args, &block)
silence(:stdout){ generator.send(*args, &block) }