Merge branch 'main' into button-to-authenticity-token
This commit is contained in:
commit
b667e48b22
@ -17,7 +17,7 @@
|
||||
s.email = ["pratiknaik@gmail.com", "david@loudthinking.com"]
|
||||
s.homepage = "https://rubyonrails.org"
|
||||
|
||||
s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/assets/javascripts/action_cable.js"]
|
||||
s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/assets/javascripts/*.js"]
|
||||
s.require_path = "lib"
|
||||
|
||||
s.metadata = {
|
||||
|
@ -435,6 +435,9 @@ module ActionMailer
|
||||
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
|
||||
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
|
||||
#
|
||||
# * <tt>delivery_job</tt> - The job class used with <tt>deliver_later</tt>. Defaults to
|
||||
# +ActionMailer::MailDeliveryJob+.
|
||||
#
|
||||
# * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>.
|
||||
class Base < AbstractController::Base
|
||||
include DeliveryMethods
|
||||
|
@ -1,3 +1,29 @@
|
||||
* Raise `ActionController::Redirecting::UnsafeRedirectError` for unsafe `redirect_to` redirects.
|
||||
|
||||
This allows `rescue_from` to be used to add a default fallback route:
|
||||
|
||||
```ruby
|
||||
rescue_from ActionController::Redirecting::UnsafeRedirectError do
|
||||
redirect_to root_url
|
||||
end
|
||||
```
|
||||
|
||||
*Kasper Timm Hansen*, *Chris Oliver*
|
||||
|
||||
* Add `url_from` to verify a redirect location is internal.
|
||||
|
||||
Takes the open redirect protection from `redirect_to` so users can wrap a
|
||||
param, and fall back to an alternate redirect URL when the param provided
|
||||
one is unsafe.
|
||||
|
||||
```ruby
|
||||
def create
|
||||
redirect_to url_from(params[:redirect_url]) || root_url
|
||||
end
|
||||
```
|
||||
|
||||
*dmcge*, *Kasper Timm Hansen*
|
||||
|
||||
* Allow Capybara driver name overrides in `SystemTestCase::driven_by`
|
||||
|
||||
Allow users to prevent conflicts among drivers that use the same driver
|
||||
|
@ -148,6 +148,7 @@ def process(action, *args)
|
||||
|
||||
@_response_body = nil
|
||||
|
||||
ActiveSupport::ExecutionContext[:controller] = self
|
||||
process_action(action_name, *args)
|
||||
end
|
||||
|
||||
|
@ -37,7 +37,6 @@ module ActionController
|
||||
autoload :Logging
|
||||
autoload :MimeResponds
|
||||
autoload :ParamsWrapper
|
||||
autoload :QueryTags
|
||||
autoload :Redirecting
|
||||
autoload :Renderers
|
||||
autoload :Rendering
|
||||
|
@ -1,16 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ActionController
|
||||
module QueryTags # :nodoc:
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
around_action :expose_controller_to_query_logs
|
||||
end
|
||||
|
||||
private
|
||||
def expose_controller_to_query_logs(&block)
|
||||
ActiveRecord::QueryLogs.set_context(controller: self, &block)
|
||||
end
|
||||
end
|
||||
end
|
@ -7,6 +7,8 @@ module Redirecting
|
||||
include AbstractController::Logger
|
||||
include ActionController::UrlFor
|
||||
|
||||
class UnsafeRedirectError < StandardError; end
|
||||
|
||||
included do
|
||||
mattr_accessor :raise_on_open_redirects, default: false
|
||||
end
|
||||
@ -61,20 +63,34 @@ module Redirecting
|
||||
#
|
||||
# redirect_to post_url(@post) and return
|
||||
#
|
||||
# Passing user input directly into +redirect_to+ is considered dangerous (e.g. `redirect_to(params[:location])`).
|
||||
# Always use regular expressions or a permitted list when redirecting to a user specified location.
|
||||
# === Open Redirect protection
|
||||
#
|
||||
# By default, Rails protects against redirecting to external hosts for your app's safety, so called open redirects.
|
||||
# Note: this was a new default in Rails 7.0, after upgrading opt-in by uncommenting the line with +raise_on_open_redirects+ in <tt>config/initializers/new_framework_defaults_7_0.rb</tt>
|
||||
#
|
||||
# Here #redirect_to automatically validates the potentially-unsafe URL:
|
||||
#
|
||||
# redirect_to params[:redirect_url]
|
||||
#
|
||||
# Raises UnsafeRedirectError in the case of an unsafe redirect.
|
||||
#
|
||||
# To allow any external redirects pass `allow_other_host: true`, though using a user-provided param in that case is unsafe.
|
||||
#
|
||||
# redirect_to "https://rubyonrails.org", allow_other_host: true
|
||||
#
|
||||
# See #url_from for more information on what an internal and safe URL is, or how to fall back to an alternate redirect URL in the unsafe case.
|
||||
def redirect_to(options = {}, response_options = {})
|
||||
response_options[:allow_other_host] ||= _allow_other_host unless response_options.key?(:allow_other_host)
|
||||
|
||||
raise ActionControllerError.new("Cannot redirect to nil!") unless options
|
||||
raise AbstractController::DoubleRenderError if response_body
|
||||
|
||||
allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
|
||||
|
||||
self.status = _extract_redirect_to_status(options, response_options)
|
||||
self.location = _compute_safe_redirect_to_location(request, options, response_options)
|
||||
self.location = _enforce_open_redirect_protection(_compute_redirect_to_location(request, options), allow_other_host: allow_other_host)
|
||||
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
|
||||
end
|
||||
|
||||
# Soft deprecated alias for <tt>redirect_back_or_to</tt> where the fallback_location location is supplied as a keyword argument instead
|
||||
# Soft deprecated alias for #redirect_back_or_to where the +fallback_location+ location is supplied as a keyword argument instead
|
||||
# of the first positional argument.
|
||||
def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
|
||||
redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
|
||||
@ -103,23 +119,11 @@ def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **arg
|
||||
# All other options that can be passed to #redirect_to are accepted as
|
||||
# options and the behavior is identical.
|
||||
def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
|
||||
location = request.referer || fallback_location
|
||||
location = fallback_location unless allow_other_host || _url_host_allowed?(request.referer)
|
||||
allow_other_host = true if _allow_other_host && !allow_other_host # if the fallback is an open redirect
|
||||
|
||||
redirect_to location, allow_other_host: allow_other_host, **options
|
||||
end
|
||||
|
||||
def _compute_safe_redirect_to_location(request, options, response_options)
|
||||
location = _compute_redirect_to_location(request, options)
|
||||
|
||||
if response_options[:allow_other_host] || _url_host_allowed?(location)
|
||||
location
|
||||
if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
|
||||
redirect_to request.referer, allow_other_host: allow_other_host, **options
|
||||
else
|
||||
raise(ArgumentError, <<~MSG.squish)
|
||||
Unsafe redirect #{location.truncate(100).inspect},
|
||||
use :allow_other_host to redirect anyway.
|
||||
MSG
|
||||
# The method level `allow_other_host` doesn't apply in the fallback case, omit and let the `redirect_to` handling take over.
|
||||
redirect_to fallback_location, **options
|
||||
end
|
||||
end
|
||||
|
||||
@ -143,6 +147,30 @@ def _compute_redirect_to_location(request, options) # :nodoc:
|
||||
module_function :_compute_redirect_to_location
|
||||
public :_compute_redirect_to_location
|
||||
|
||||
# Verifies the passed +location+ is an internal URL that's safe to redirect to and returns it, or nil if not.
|
||||
# Useful to wrap a params provided redirect URL and fallback to an alternate URL to redirect to:
|
||||
#
|
||||
# redirect_to url_from(params[:redirect_url]) || root_url
|
||||
#
|
||||
# The +location+ is considered internal, and safe, if it's on the same host as <tt>request.host</tt>:
|
||||
#
|
||||
# # If request.host is example.com:
|
||||
# url_from("https://example.com/profile") # => "https://example.com/profile"
|
||||
# url_from("http://example.com/profile") # => "http://example.com/profile"
|
||||
# url_from("http://evil.com/profile") # => nil
|
||||
#
|
||||
# Subdomains are considered part of the host:
|
||||
#
|
||||
# # If request.host is on https://example.com or https://app.example.com, you'd get:
|
||||
# url_from("https://dev.example.com/profile") # => nil
|
||||
#
|
||||
# NOTE: there's a similarity with {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor#url_for], which generates an internal URL from various options from within the app, e.g. <tt>url_for(@post)</tt>.
|
||||
# However, #url_from is meant to take an external parameter to verify as in <tt>url_from(params[:redirect_url])</tt>.
|
||||
def url_from(location)
|
||||
location = location.presence
|
||||
location if location && _url_host_allowed?(location)
|
||||
end
|
||||
|
||||
private
|
||||
def _allow_other_host
|
||||
!raise_on_open_redirects
|
||||
@ -158,6 +186,14 @@ def _extract_redirect_to_status(options, response_options)
|
||||
end
|
||||
end
|
||||
|
||||
def _enforce_open_redirect_protection(location, allow_other_host:)
|
||||
if allow_other_host || _url_host_allowed?(location)
|
||||
location
|
||||
else
|
||||
raise UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
|
||||
end
|
||||
end
|
||||
|
||||
def _url_host_allowed?(url)
|
||||
URI(url.to_s).host == request.host
|
||||
rescue ArgumentError, URI::Error
|
||||
|
@ -113,10 +113,6 @@ class Railtie < Rails::Railtie # :nodoc:
|
||||
if query_logs_tags_enabled
|
||||
app.config.active_record.query_log_tags += [:controller, :action]
|
||||
|
||||
ActiveSupport.on_load(:action_controller) do
|
||||
include ActionController::QueryTags
|
||||
end
|
||||
|
||||
ActiveSupport.on_load(:active_record) do
|
||||
ActiveRecord::QueryLogs.taggings.merge!(
|
||||
controller: ->(context) { context[:controller]&.controller_name },
|
||||
|
@ -88,6 +88,10 @@ def unsafe_redirect_back
|
||||
redirect_back_or_to "http://www.rubyonrails.org/"
|
||||
end
|
||||
|
||||
def safe_redirect_with_fallback
|
||||
redirect_to url_from(params[:redirect_url]) || "/fallback"
|
||||
end
|
||||
|
||||
def redirect_back_with_explicit_fallback_kwarg
|
||||
redirect_back(fallback_location: "/things/stuff", status: 307)
|
||||
end
|
||||
@ -478,27 +482,41 @@ def test_redirect_to_with_block_and_accepted_options
|
||||
|
||||
def test_unsafe_redirect
|
||||
with_raise_on_open_redirects do
|
||||
error = assert_raise(ArgumentError) do
|
||||
error = assert_raise(ActionController::Redirecting::UnsafeRedirectError) do
|
||||
get :unsafe_redirect
|
||||
end
|
||||
|
||||
assert_equal(<<~MSG.squish, error.message)
|
||||
Unsafe redirect \"http://www.rubyonrails.org/\",
|
||||
use :allow_other_host to redirect anyway.
|
||||
MSG
|
||||
assert_equal "Unsafe redirect to \"http://www.rubyonrails.org/\", pass allow_other_host: true to redirect anyway.", error.message
|
||||
end
|
||||
end
|
||||
|
||||
def test_unsafe_redirect_back
|
||||
with_raise_on_open_redirects do
|
||||
error = assert_raise(ArgumentError) do
|
||||
error = assert_raise(ActionController::Redirecting::UnsafeRedirectError) do
|
||||
get :unsafe_redirect_back
|
||||
end
|
||||
|
||||
assert_equal(<<~MSG.squish, error.message)
|
||||
Unsafe redirect \"http://www.rubyonrails.org/\",
|
||||
use :allow_other_host to redirect anyway.
|
||||
MSG
|
||||
assert_equal "Unsafe redirect to \"http://www.rubyonrails.org/\", pass allow_other_host: true to redirect anyway.", error.message
|
||||
end
|
||||
end
|
||||
|
||||
def test_url_from
|
||||
with_raise_on_open_redirects do
|
||||
get :safe_redirect_with_fallback, params: { redirect_url: "http://test.host/app" }
|
||||
assert_response :redirect
|
||||
assert_redirected_to "http://test.host/app"
|
||||
end
|
||||
end
|
||||
|
||||
def test_url_from_fallback
|
||||
with_raise_on_open_redirects do
|
||||
get :safe_redirect_with_fallback, params: { redirect_url: "http://www.rubyonrails.org/" }
|
||||
assert_response :redirect
|
||||
assert_redirected_to "http://test.host/fallback"
|
||||
|
||||
get :safe_redirect_with_fallback, params: { redirect_url: "" }
|
||||
assert_response :redirect
|
||||
assert_redirected_to "http://test.host/fallback"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -13,6 +13,15 @@
|
||||
|
||||
*Sean Doyle*
|
||||
|
||||
* Support rendering `<form>` elements _without_ `[action]` attributes by:
|
||||
|
||||
* `form_with url: false` or `form_with ..., html: { action: false }`
|
||||
* `form_for ..., url: false` or `form_for ..., html: { action: false }`
|
||||
* `form_tag false` or `form_tag ..., action: false`
|
||||
* `button_to "...", false` or `button_to(false) { ... }`
|
||||
|
||||
*Sean Doyle*
|
||||
|
||||
* Add `:day_format` option to `date_select`
|
||||
|
||||
date_select("article", "written_on", day_format: ->(day) { day.ordinalize })
|
||||
|
@ -282,6 +282,12 @@ module FormHelper
|
||||
# ...
|
||||
# <% end %>
|
||||
#
|
||||
# You can omit the <tt>action</tt> attribute by passing <tt>url: false</tt>:
|
||||
#
|
||||
# <%= form_for(@post, url: false) do |f| %>
|
||||
# ...
|
||||
# <% end %>
|
||||
#
|
||||
# You can also set the answer format, like this:
|
||||
#
|
||||
# <%= form_for(@post, format: :json) do |f| %>
|
||||
@ -449,7 +455,7 @@ def form_for(record, options = {}, &block)
|
||||
output = capture(builder, &block)
|
||||
html_options[:multipart] ||= builder.multipart?
|
||||
|
||||
html_options = html_options_for_form(options[:url] || {}, html_options)
|
||||
html_options = html_options_for_form(options.fetch(:url, {}), html_options)
|
||||
form_tag_with_body(html_options, output)
|
||||
end
|
||||
|
||||
@ -465,10 +471,12 @@ def apply_form_for_options!(record, object, options) # :nodoc:
|
||||
method: method
|
||||
)
|
||||
|
||||
options[:url] ||= if options.key?(:format)
|
||||
polymorphic_path(record, format: options.delete(:format))
|
||||
else
|
||||
polymorphic_path(record, {})
|
||||
if options[:url] != false
|
||||
options[:url] ||= if options.key?(:format)
|
||||
polymorphic_path(record, format: options.delete(:format))
|
||||
else
|
||||
polymorphic_path(record, {})
|
||||
end
|
||||
end
|
||||
end
|
||||
private :apply_form_for_options!
|
||||
@ -487,6 +495,15 @@ def apply_form_for_options!(record, object, options) # :nodoc:
|
||||
# <form action="/posts" method="post">
|
||||
# <input type="text" name="title">
|
||||
# </form>
|
||||
|
||||
# # With an intentionally empty URL:
|
||||
# <%= form_with url: false do |form| %>
|
||||
# <%= form.text_field :title %>
|
||||
# <% end %>
|
||||
# # =>
|
||||
# <form method="post" data-remote="true">
|
||||
# <input type="text" name="title">
|
||||
# </form>
|
||||
#
|
||||
# # Adding a scope prefixes the input field names:
|
||||
# <%= form_with scope: :post, url: posts_path do |form| %>
|
||||
@ -744,7 +761,9 @@ def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
|
||||
options[:skip_default_ids] = !form_with_generates_ids
|
||||
|
||||
if model
|
||||
url ||= polymorphic_path(model, format: format)
|
||||
if url != false
|
||||
url ||= polymorphic_path(model, format: format)
|
||||
end
|
||||
|
||||
model = model.last if model.is_a?(Array)
|
||||
scope ||= model_name_from_record_or_class(model).param_key
|
||||
@ -1559,7 +1578,11 @@ def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, loc
|
||||
|
||||
# The following URL is unescaped, this is just a hash of options, and it is the
|
||||
# responsibility of the caller to escape all the values.
|
||||
html_options[:action] = url_for(url_for_options || {})
|
||||
if url_for_options == false || html_options[:action] == false
|
||||
html_options.delete(:action)
|
||||
else
|
||||
html_options[:action] = url_for(url_for_options || {})
|
||||
end
|
||||
html_options[:"accept-charset"] = "UTF-8"
|
||||
html_options[:"data-remote"] = true unless local
|
||||
|
||||
|
@ -62,6 +62,9 @@ module FormTagHelper
|
||||
#
|
||||
# <%= form_tag('/posts', remote: true) %>
|
||||
# # => <form action="/posts" method="post" data-remote="true">
|
||||
|
||||
# form_tag(false, method: :get)
|
||||
# # => <form method="get">
|
||||
#
|
||||
# form_tag('http://far.away.com/form', authenticity_token: false)
|
||||
# # form without authenticity token
|
||||
@ -875,7 +878,11 @@ def html_options_for_form(url_for_options, options)
|
||||
html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
|
||||
# The following URL is unescaped, this is just a hash of options, and it is the
|
||||
# responsibility of the caller to escape all the values.
|
||||
html_options["action"] = url_for(url_for_options)
|
||||
if url_for_options == false || html_options["action"] == false
|
||||
html_options.delete("action")
|
||||
else
|
||||
html_options["action"] = url_for(url_for_options)
|
||||
end
|
||||
html_options["accept-charset"] = "UTF-8"
|
||||
|
||||
html_options["data-remote"] = true if html_options.delete("remote")
|
||||
|
@ -235,6 +235,20 @@ def method_missing(called, *args, **options, &block)
|
||||
# # A void element:
|
||||
# tag.br # => <br>
|
||||
#
|
||||
# === Building HTML attributes
|
||||
#
|
||||
# Transforms a Hash into HTML attributes, ready to be interpolated into
|
||||
# ERB. Includes or omits boolean attributes based on their truthiness.
|
||||
# Transforms keys nested within
|
||||
# <tt>aria:</tt> or <tt>data:</tt> objects into `aria-` and `data-`
|
||||
# prefixed attributes:
|
||||
#
|
||||
# <input <%= tag.attributes(type: :text, aria: { label: "Search" }) %>>
|
||||
# # => <input type="text" aria-label="Search">
|
||||
#
|
||||
# <button <%= tag.attributes id: "call-to-action", disabled: false, aria: { expanded: false } %> class="primary">Get Started!</button>
|
||||
# => <button id="call-to-action" aria-expanded="false" class="primary">Get Started!</button>
|
||||
#
|
||||
# === Legacy syntax
|
||||
#
|
||||
# The following format is for legacy syntax support. It will be deprecated in future versions of Rails.
|
||||
|
@ -238,7 +238,15 @@ def link_to(name = nil, options = nil, html_options = nil, &block)
|
||||
# HTTP verb via the +:method+ option within +html_options+.
|
||||
#
|
||||
# ==== Options
|
||||
# The +options+ hash accepts the same options as +url_for+.
|
||||
# The +options+ hash accepts the same options as +url_for+. To generate a
|
||||
# <tt><form></tt> element without an <tt>[action]</tt> attribute, pass
|
||||
# <tt>false</tt>:
|
||||
#
|
||||
# <%= button_to "New", false %>
|
||||
# # => "<form method="post" class="button_to">
|
||||
# # <button type="submit">New</button>
|
||||
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
|
||||
# # </form>"
|
||||
#
|
||||
# Most values in +html_options+ are passed through to the button element,
|
||||
# but there are a few special options:
|
||||
@ -324,11 +332,15 @@ def link_to(name = nil, options = nil, html_options = nil, &block)
|
||||
# #
|
||||
def button_to(name = nil, options = nil, html_options = nil, &block)
|
||||
html_options, options = options, name if block_given?
|
||||
options ||= {}
|
||||
html_options ||= {}
|
||||
html_options = html_options.stringify_keys
|
||||
|
||||
url = options.is_a?(String) ? options : url_for(options)
|
||||
url =
|
||||
case options
|
||||
when FalseClass then nil
|
||||
else url_for(options)
|
||||
end
|
||||
|
||||
remote = html_options.delete("remote")
|
||||
params = html_options.delete("params")
|
||||
|
||||
|
@ -53,7 +53,7 @@ def form_text(action = "http://www.example.com", local: false, **options)
|
||||
|
||||
method = method.to_s == "get" ? "get" : "post"
|
||||
|
||||
txt = +%{<form accept-charset="UTF-8" action="#{action}"}
|
||||
txt = +%{<form accept-charset="UTF-8"} + (action ? %{ action="#{action}"} : "")
|
||||
txt << %{ enctype="multipart/form-data"} if enctype
|
||||
txt << %{ data-remote="true"} unless local
|
||||
txt << %{ class="#{html_class}"} if html_class
|
||||
@ -107,6 +107,22 @@ def test_form_with_with_method_delete
|
||||
assert_dom_equal expected, actual
|
||||
end
|
||||
|
||||
def test_form_with_false_url
|
||||
actual = form_with(url: false)
|
||||
|
||||
expected = whole_form(false)
|
||||
|
||||
assert_dom_equal expected, actual
|
||||
end
|
||||
|
||||
def test_form_with_false_action
|
||||
actual = form_with(html: { action: false })
|
||||
|
||||
expected = whole_form(false)
|
||||
|
||||
assert_dom_equal expected, actual
|
||||
end
|
||||
|
||||
def test_form_with_with_local_true
|
||||
actual = form_with(local: true)
|
||||
|
||||
@ -445,6 +461,22 @@ def test_form_with_general_attributes
|
||||
assert_dom_equal expected, output_buffer
|
||||
end
|
||||
|
||||
def test_form_with_false_url
|
||||
form_with(url: false)
|
||||
|
||||
expected = whole_form(false)
|
||||
|
||||
assert_dom_equal expected, output_buffer
|
||||
end
|
||||
|
||||
def test_form_with_model_and_false_url
|
||||
form_with(model: Post.new, url: false)
|
||||
|
||||
expected = whole_form(false)
|
||||
|
||||
assert_dom_equal expected, output_buffer
|
||||
end
|
||||
|
||||
def test_form_with_attribute_not_on_model
|
||||
form_with(model: @post) do |f|
|
||||
concat f.text_field :this_dont_exist_on_post
|
||||
@ -2398,7 +2430,7 @@ def hidden_fields(options = {})
|
||||
end
|
||||
|
||||
def form_text(action = "/", id = nil, html_class = nil, local = nil, multipart = nil, method = nil)
|
||||
txt = +%{<form accept-charset="UTF-8" action="#{action}"}
|
||||
txt = +%{<form accept-charset="UTF-8"} + (action ? %{ action="#{action}"} : "")
|
||||
txt << %{ enctype="multipart/form-data"} if multipart
|
||||
txt << %{ data-remote="true"} unless local
|
||||
txt << %{ class="#{html_class}"} if html_class
|
||||
|
@ -1617,6 +1617,24 @@ def test_form_for_id
|
||||
assert_dom_equal expected, output_buffer
|
||||
end
|
||||
|
||||
def test_form_for_false_url
|
||||
form_for(Post.new, url: false) do |form|
|
||||
end
|
||||
|
||||
expected = whole_form(false, "new_post", "new_post")
|
||||
|
||||
assert_dom_equal expected, output_buffer
|
||||
end
|
||||
|
||||
def test_form_for_false_action
|
||||
form_for(Post.new, html: { action: false }) do |form|
|
||||
end
|
||||
|
||||
expected = whole_form(false, "new_post", "new_post")
|
||||
|
||||
assert_dom_equal expected, output_buffer
|
||||
end
|
||||
|
||||
def test_field_id_with_model
|
||||
value = field_id(Post.new, :title)
|
||||
|
||||
@ -3738,7 +3756,7 @@ def hidden_fields(options = {})
|
||||
end
|
||||
|
||||
def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
|
||||
txt = +%{<form accept-charset="UTF-8" action="#{action}"}
|
||||
txt = +%{<form accept-charset="UTF-8"} + (action ? %{ action="#{action}"} : "")
|
||||
txt << %{ enctype="multipart/form-data"} if multipart
|
||||
txt << %{ data-remote="true"} if remote
|
||||
txt << %{ class="#{html_class}"} if html_class
|
||||
|
@ -42,7 +42,7 @@ def form_text(action = "http://www.example.com", options = {})
|
||||
|
||||
method = method.to_s == "get" ? "get" : "post"
|
||||
|
||||
txt = +%{<form accept-charset="UTF-8" action="#{action}"}
|
||||
txt = +%{<form accept-charset="UTF-8"} + (action ? %{ action="#{action}"} : "")
|
||||
txt << %{ enctype="multipart/form-data"} if enctype
|
||||
txt << %{ data-remote="true"} if remote
|
||||
txt << %{ class="#{html_class}"} if html_class
|
||||
@ -138,6 +138,20 @@ def test_form_tag_with_remote_false
|
||||
assert_dom_equal expected, actual
|
||||
end
|
||||
|
||||
def test_form_tag_with_false_url_for_options
|
||||
actual = form_tag(false)
|
||||
|
||||
expected = whole_form(false)
|
||||
assert_dom_equal expected, actual
|
||||
end
|
||||
|
||||
def test_form_tag_with_false_action
|
||||
actual = form_tag({}, action: false)
|
||||
|
||||
expected = whole_form(false)
|
||||
assert_dom_equal expected, actual
|
||||
end
|
||||
|
||||
def test_form_tag_enforce_utf8_true
|
||||
actual = form_tag({}, { enforce_utf8: true })
|
||||
expected = whole_form("http://www.example.com", enforce_utf8: true)
|
||||
|
@ -194,6 +194,20 @@ def test_button_to_with_path
|
||||
)
|
||||
end
|
||||
|
||||
def test_button_to_with_false_url
|
||||
assert_dom_equal(
|
||||
%{<form method="post" class="button_to"><button type="submit">Hello</button></form>},
|
||||
button_to("Hello", false)
|
||||
)
|
||||
end
|
||||
|
||||
def test_button_to_with_false_url_and_block
|
||||
assert_dom_equal(
|
||||
%{<form method="post" class="button_to"><button type="submit">Hello</button></form>},
|
||||
button_to(false) { "Hello" }
|
||||
)
|
||||
end
|
||||
|
||||
def test_button_to_with_straight_url_and_request_forgery
|
||||
self.request_forgery = true
|
||||
|
||||
|
@ -54,6 +54,7 @@ def perform(*)
|
||||
|
||||
private
|
||||
def _perform_job
|
||||
ActiveSupport::ExecutionContext[:job] = self
|
||||
run_callbacks :perform do
|
||||
perform(*arguments)
|
||||
end
|
||||
|
@ -1,16 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ActiveJob
|
||||
module QueryTags # :nodoc:
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
around_perform :expose_job_to_query_logs
|
||||
end
|
||||
|
||||
private
|
||||
def expose_job_to_query_logs(&block)
|
||||
ActiveRecord::QueryLogs.set_context(job: self, &block)
|
||||
end
|
||||
end
|
||||
end
|
@ -65,10 +65,6 @@ class Railtie < Rails::Railtie # :nodoc:
|
||||
if query_logs_tags_enabled
|
||||
app.config.active_record.query_log_tags << :job
|
||||
|
||||
ActiveSupport.on_load(:active_job) do
|
||||
include ActiveJob::QueryTags
|
||||
end
|
||||
|
||||
ActiveSupport.on_load(:active_record) do
|
||||
ActiveRecord::QueryLogs.taggings[:job] = ->(context) { context[:job].class.name if context[:job] }
|
||||
end
|
||||
|
@ -109,7 +109,7 @@ def queue_adapter_for_test
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
|
||||
# +:only+ and +:except+ options accept Class, Array of Class or Proc. When passed a Proc,
|
||||
# a hash containing the job's class and it's argument are passed as argument.
|
||||
#
|
||||
# Asserts the number of times a job is enqueued to a specific queue by passing +:queue+ option.
|
||||
@ -168,7 +168,7 @@ def assert_enqueued_jobs(number, only: nil, except: nil, queue: nil, &block)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
|
||||
# +:only+ and +:except+ options accept Class, Array of Class or Proc. When passed a Proc,
|
||||
# a hash containing the job's class and it's argument are passed as argument.
|
||||
#
|
||||
# Asserts that no jobs are enqueued to a specific queue by passing +:queue+ option
|
||||
@ -325,7 +325,7 @@ def assert_performed_jobs(number, only: nil, except: nil, queue: nil, &block)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
|
||||
# +:only+ and +:except+ options accept Class, Array of Class or Proc. When passed a Proc,
|
||||
# an instance of the job will be passed as argument.
|
||||
#
|
||||
# If the +:queue+ option is specified,
|
||||
@ -417,8 +417,20 @@ def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil, priority: nil
|
||||
end
|
||||
end
|
||||
|
||||
matching_class = potential_matches.select do |enqueued_job|
|
||||
enqueued_job["job_class"] == job.to_s
|
||||
end
|
||||
|
||||
message = +"No enqueued job found with #{expected}"
|
||||
message << "\n\nPotential matches: #{potential_matches.join("\n")}" if potential_matches.present?
|
||||
if potential_matches.empty?
|
||||
message << "\n\nNo jobs were enqueued"
|
||||
elsif matching_class.empty?
|
||||
message << "\n\nNo jobs of class #{expected[:job]} were enqueued, job classes enqueued: "
|
||||
message << potential_matches.map { |job| job["job_class"] }.join(", ")
|
||||
else
|
||||
message << "\n\nPotential matches: #{matching_class.join("\n")}"
|
||||
end
|
||||
|
||||
assert matching_job, message
|
||||
instantiate_job(matching_job)
|
||||
end
|
||||
@ -507,8 +519,20 @@ def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, priority: ni
|
||||
end
|
||||
end
|
||||
|
||||
matching_class = potential_matches.select do |enqueued_job|
|
||||
enqueued_job["job_class"] == job.to_s
|
||||
end
|
||||
|
||||
message = +"No performed job found with #{expected}"
|
||||
message << "\n\nPotential matches: #{potential_matches.join("\n")}" if potential_matches.present?
|
||||
if potential_matches.empty?
|
||||
message << "\n\nNo jobs were performed"
|
||||
elsif matching_class.empty?
|
||||
message << "\n\nNo jobs of class #{expected[:job]} were performed, job classes performed: "
|
||||
message << potential_matches.map { |job| job["job_class"] }.join(", ")
|
||||
else
|
||||
message << "\n\nPotential matches: #{matching_class.join("\n")}"
|
||||
end
|
||||
|
||||
assert matching_job, message
|
||||
|
||||
instantiate_job(matching_job)
|
||||
@ -555,7 +579,7 @@ def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, priority: ni
|
||||
# assert_performed_jobs 1
|
||||
# end
|
||||
#
|
||||
# +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
|
||||
# +:only+ and +:except+ options accept Class, Array of Class or Proc. When passed a Proc,
|
||||
# an instance of the job will be passed as argument.
|
||||
#
|
||||
# If the +:queue+ option is specified,
|
||||
|
@ -698,10 +698,35 @@ def test_assert_enqueued_with_failure_with_global_id_args
|
||||
HelloJob.perform_later(ricardo)
|
||||
end
|
||||
end
|
||||
|
||||
assert_match(/No enqueued job found with {:job=>HelloJob, :args=>\[#{wilma.inspect}\]}/, error.message)
|
||||
assert_match(/Potential matches: {.*?:job=>HelloJob, :args=>\[#<Person.* @id="9">\], :queue=>"default".*?}/, error.message)
|
||||
end
|
||||
|
||||
def test_show_jobs_that_are_enqueued_when_job_is_not_queued_at_all
|
||||
ricardo = Person.new(9)
|
||||
wilma = Person.new(11)
|
||||
|
||||
error = assert_raise ActiveSupport::TestCase::Assertion do
|
||||
assert_enqueued_with(job: MultipleKwargsJob, args: [wilma]) do
|
||||
HelloJob.perform_later(ricardo)
|
||||
end
|
||||
end
|
||||
|
||||
assert_match(/No enqueued job found with {:job=>MultipleKwargsJob, :args=>\[#{wilma.inspect}\]}/, error.message)
|
||||
assert_match(/No jobs of class MultipleKwargsJob were enqueued, job classes enqueued: HelloJob/, error.message)
|
||||
end
|
||||
|
||||
def test_shows_no_jobs_enqueued_when_there_are_no_jobs
|
||||
error = assert_raise ActiveSupport::TestCase::Assertion do
|
||||
assert_enqueued_with(job: HelloJob, args: []) do
|
||||
end
|
||||
end
|
||||
|
||||
assert_match(/No enqueued job found with {:job=>HelloJob, :args=>\[\]}/, error.message)
|
||||
assert_match(/No jobs were enqueued/, error.message)
|
||||
end
|
||||
|
||||
def test_assert_enqueued_with_failure_with_no_block_with_global_id_args
|
||||
ricardo = Person.new(9)
|
||||
wilma = Person.new(11)
|
||||
@ -1954,6 +1979,28 @@ def test_assert_performed_with_without_block_failure_with_global_id_args
|
||||
assert_match(/Potential matches: {.*?:job=>HelloJob, :args=>\[#<Person.* @id="9">\], :queue=>"default".*?}/, error.message)
|
||||
end
|
||||
|
||||
def test_assert_performed_says_no_jobs_performed
|
||||
error = assert_raise ActiveSupport::TestCase::Assertion do
|
||||
assert_performed_with(job: HelloJob, args: [])
|
||||
end
|
||||
|
||||
assert_match(/No performed job found with {:job=>HelloJob, :args=>\[\]}/, error.message)
|
||||
assert_match(/No jobs were performed/, error.message)
|
||||
end
|
||||
|
||||
def test_assert_performed_when_not_matching_the_class_shows_alteratives
|
||||
ricardo = Person.new(9)
|
||||
wilma = Person.new(11)
|
||||
HelloJob.perform_later(ricardo)
|
||||
perform_enqueued_jobs
|
||||
error = assert_raise ActiveSupport::TestCase::Assertion do
|
||||
assert_performed_with(job: MultipleKwargsJob, args: [wilma])
|
||||
end
|
||||
|
||||
assert_match(/No performed job found with {:job=>MultipleKwargsJob, :args=>\[#<Person.* @id=11>\]}/, error.message)
|
||||
assert_match(/No jobs of class MultipleKwargsJob were performed, job classes performed: HelloJob/, error.message)
|
||||
end
|
||||
|
||||
def test_assert_performed_with_does_not_change_jobs_count
|
||||
assert_performed_with(job: HelloJob) do
|
||||
HelloJob.perform_later
|
||||
|
@ -208,7 +208,7 @@ def attribute_method_affix(*affixes)
|
||||
# person.nickname_short? # => true
|
||||
def alias_attribute(new_name, old_name)
|
||||
self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
|
||||
CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
|
||||
ActiveSupport::CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
|
||||
attribute_method_matchers.each do |matcher|
|
||||
method_name = matcher.method_name(new_name).to_s
|
||||
target_name = matcher.method_name(old_name).to_s
|
||||
@ -274,7 +274,7 @@ def attribute_alias(name)
|
||||
# end
|
||||
# end
|
||||
def define_attribute_methods(*attr_names)
|
||||
CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
|
||||
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
|
||||
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
|
||||
end
|
||||
end
|
||||
@ -309,7 +309,7 @@ def define_attribute_methods(*attr_names)
|
||||
# person.name # => "Bob"
|
||||
# person.name_short? # => true
|
||||
def define_attribute_method(attr_name, _owner: generated_attribute_methods)
|
||||
CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
|
||||
ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
|
||||
attribute_method_matchers.each do |matcher|
|
||||
method_name = matcher.method_name(attr_name)
|
||||
|
||||
@ -358,69 +358,6 @@ def undefine_attribute_methods
|
||||
end
|
||||
|
||||
private
|
||||
class CodeGenerator # :nodoc:
|
||||
class MethodSet
|
||||
METHOD_CACHES = Hash.new { |h, k| h[k] = Module.new }
|
||||
|
||||
def initialize(namespace)
|
||||
@cache = METHOD_CACHES[namespace]
|
||||
@sources = []
|
||||
@methods = {}
|
||||
end
|
||||
|
||||
def define_cached_method(name, as: name)
|
||||
name = name.to_sym
|
||||
as = as.to_sym
|
||||
@methods.fetch(name) do
|
||||
unless @cache.method_defined?(as)
|
||||
yield @sources
|
||||
end
|
||||
@methods[name] = as
|
||||
end
|
||||
end
|
||||
|
||||
def apply(owner, path, line)
|
||||
unless @sources.empty?
|
||||
@cache.module_eval("# frozen_string_literal: true\n" + @sources.join(";"), path, line)
|
||||
end
|
||||
@methods.each do |name, as|
|
||||
owner.define_method(name, @cache.instance_method(as))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def batch(owner, path, line)
|
||||
if owner.is_a?(CodeGenerator)
|
||||
yield owner
|
||||
else
|
||||
instance = new(owner, path, line)
|
||||
result = yield instance
|
||||
instance.execute
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(owner, path, line)
|
||||
@owner = owner
|
||||
@path = path
|
||||
@line = line
|
||||
@namespaces = Hash.new { |h, k| h[k] = MethodSet.new(k) }
|
||||
end
|
||||
|
||||
def define_cached_method(name, namespace:, as: name, &block)
|
||||
@namespaces[namespace].define_cached_method(name, as: as, &block)
|
||||
end
|
||||
|
||||
def execute
|
||||
@namespaces.each_value do |method_set|
|
||||
method_set.apply(@owner, @path, @line - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
private_constant :CodeGenerator
|
||||
|
||||
def generated_attribute_methods
|
||||
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
|
||||
end
|
||||
|
@ -1,3 +1,79 @@
|
||||
* Fix regression bug that caused ignoring additional conditions for preloading has_many-through relations.
|
||||
|
||||
Fixes #43132
|
||||
|
||||
*Alexander Pauly*
|
||||
|
||||
* Fix `has_many` inversing recursion on models with recursive associations.
|
||||
|
||||
*Gannon McGibbon*
|
||||
|
||||
* Add `accepts_nested_attributes_for` support for `delegated_type`
|
||||
|
||||
```ruby
|
||||
class Entry < ApplicationRecord
|
||||
delegated_type :entryable, types: %w[ Message Comment ]
|
||||
accepts_nested_attributes_for :entryable
|
||||
end
|
||||
|
||||
entry = Entry.create(entryable_type: 'Message', entryable_attributes: { content: 'Hello world' })
|
||||
# => #<Entry:0x00>
|
||||
# id: 1
|
||||
# entryable_id: 1,
|
||||
# entryable_type: 'Message'
|
||||
# ...>
|
||||
|
||||
entry.entryable
|
||||
# => #<Message:0x01>
|
||||
# id: 1
|
||||
# content: 'Hello world'
|
||||
# ...>
|
||||
```
|
||||
|
||||
Previously it would raise an error:
|
||||
|
||||
```ruby
|
||||
Entry.create(entryable_type: 'Message', entryable_attributes: { content: 'Hello world' })
|
||||
# ArgumentError: Cannot build association `entryable'. Are you trying to build a polymorphic one-to-one association?
|
||||
```
|
||||
|
||||
*Sjors Baltus*
|
||||
|
||||
* Use subquery for DELETE with GROUP_BY and HAVING clauses.
|
||||
|
||||
Prior to this change, deletes with GROUP_BY and HAVING were returning an error.
|
||||
|
||||
After this change, GROUP_BY and HAVING are valid clauses in DELETE queries, generating the following query:
|
||||
|
||||
```sql
|
||||
DELETE FROM "posts" WHERE "posts"."id" IN (
|
||||
SELECT "posts"."id" FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" GROUP BY "posts"."id" HAVING (count(comments.id) >= 2))
|
||||
) [["flagged", "t"]]
|
||||
```
|
||||
|
||||
*Ignacio Chiazzo Cardarello*
|
||||
|
||||
* Use subquery for UPDATE with GROUP_BY and HAVING clauses.
|
||||
|
||||
Prior to this change, updates with GROUP_BY and HAVING were being ignored, generating a SQL like this:
|
||||
|
||||
```sql
|
||||
UPDATE "posts" SET "flagged" = ? WHERE "posts"."id" IN (
|
||||
SELECT "posts"."id" FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
|
||||
) [["flagged", "t"]]
|
||||
```
|
||||
|
||||
After this change, GROUP_BY and HAVING clauses are used as a subquery in updates, like this:
|
||||
|
||||
```sql
|
||||
UPDATE "posts" SET "flagged" = ? WHERE "posts"."id" IN (
|
||||
SELECT "posts"."id" FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
|
||||
GROUP BY posts.id HAVING (count(comments.id) >= 2)
|
||||
) [["flagged", "t"]]
|
||||
```
|
||||
|
||||
*Ignacio Chiazzo Cardarello*
|
||||
|
||||
* Add support for setting the filename of the schema or structure dump in the database config.
|
||||
|
||||
Applications may now set their the filename or path of the schema / structure dump file in their database configuration.
|
||||
|
@ -59,6 +59,18 @@ def corrections
|
||||
end
|
||||
end
|
||||
|
||||
class InverseOfAssociationRecursiveError < ActiveRecordError # :nodoc:
|
||||
attr_reader :reflection
|
||||
def initialize(reflection = nil)
|
||||
if reflection
|
||||
@reflection = reflection
|
||||
super("Inverse association #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{reflection.class_name}) is recursive.")
|
||||
else
|
||||
super("Inverse association is recursive.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyThroughAssociationNotFoundError < ActiveRecordError # :nodoc:
|
||||
attr_reader :owner_class, :reflection
|
||||
|
||||
|
@ -263,7 +263,7 @@ def owner_key_type
|
||||
end
|
||||
|
||||
def reflection_scope
|
||||
@reflection_scope ||= reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass).inject(&:merge!) || klass.unscoped
|
||||
@reflection_scope ||= reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass).inject(klass.unscoped, &:merge!)
|
||||
end
|
||||
|
||||
def build_scope
|
||||
|
@ -778,7 +778,7 @@ def rename_column(table_name, column_name, new_column_name)
|
||||
#
|
||||
# CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
|
||||
#
|
||||
# Note: SQLite doesn't support index length.
|
||||
# Note: only supported by MySQL
|
||||
#
|
||||
# ====== Creating an index with a sort order (desc or asc, asc is the default)
|
||||
#
|
||||
|
@ -641,6 +641,14 @@ def database_version # :nodoc:
|
||||
def check_version # :nodoc:
|
||||
end
|
||||
|
||||
# Returns the version identifier of the schema currently available in
|
||||
# the database. This is generally equal to the number of the highest-
|
||||
# numbered migration that has been executed, or 0 if no schema
|
||||
# information is present / the database is empty.
|
||||
def schema_version
|
||||
migration_context.current_version
|
||||
end
|
||||
|
||||
def field_ordered_value(column, values) # :nodoc:
|
||||
node = Arel::Nodes::Case.new(column)
|
||||
values.each.with_index(1) do |value, order|
|
||||
|
@ -208,7 +208,7 @@ def ignored_table?(table_name)
|
||||
end
|
||||
|
||||
def reset_version!
|
||||
@version = connection.migration_context.current_version
|
||||
@version = connection.schema_version
|
||||
end
|
||||
|
||||
def derive_columns_hash_and_deduplicate_values
|
||||
|
@ -137,6 +137,21 @@ module ActiveRecord
|
||||
# end
|
||||
#
|
||||
# Now you can list a bunch of entries, call +Entry#title+, and polymorphism will provide you with the answer.
|
||||
#
|
||||
# == Nested Attributes
|
||||
#
|
||||
# Enabling nested attributes on a delegated_type association allows you to
|
||||
# create the entry and message in one go:
|
||||
#
|
||||
# class Entry < ApplicationRecord
|
||||
# delegated_type :entryable, types: %w[ Message Comment ]
|
||||
# accepts_nested_attributes_for :entryable
|
||||
# end
|
||||
#
|
||||
# params = { entry: { entryable_type: 'Message', entryable_attributes: { subject: 'Smiling' } } }
|
||||
# entry = Entry.create(params[:entry])
|
||||
# entry.entryable.id # => 2
|
||||
# entry.entryable.subject # => 'Smiling'
|
||||
module DelegatedType
|
||||
# Defines this as a class that'll delegate its type for the passed +role+ to the class references in +types+.
|
||||
# That'll create a polymorphic +belongs_to+ relationship to that +role+, and it'll add all the delegated
|
||||
@ -207,6 +222,10 @@ def define_delegated_type_methods(role, types:, options:)
|
||||
public_send("#{role}_class").model_name.singular.inquiry
|
||||
end
|
||||
|
||||
define_method "build_#{role}" do |*params|
|
||||
public_send("#{role}=", public_send("#{role}_class").new(*params))
|
||||
end
|
||||
|
||||
types.each do |type|
|
||||
scope_name = type.tableize.tr("/", "_")
|
||||
singular = scope_name.singularize
|
||||
|
@ -44,17 +44,17 @@ module ActiveRecord
|
||||
# ]
|
||||
# ActiveRecord::QueryLogs.tags = tags
|
||||
#
|
||||
# The QueryLogs +context+ can be manipulated via the +set_context+ method.
|
||||
# The QueryLogs +context+ can be manipulated via the +ActiveSupport::ExecutionContext.set+ method.
|
||||
#
|
||||
# Temporary updates limited to the execution of a block:
|
||||
#
|
||||
# ActiveRecord::QueryLogs.set_context(foo: Bar.new) do
|
||||
# ActiveSupport::ExecutionContext.set(foo: Bar.new) do
|
||||
# posts = Post.all
|
||||
# end
|
||||
#
|
||||
# Direct updates to a context value:
|
||||
#
|
||||
# ActiveRecord::QueryLogs.set_context(foo: Bar.new)
|
||||
# ActiveSupport::ExecutionContext[:foo] = Bar.new
|
||||
#
|
||||
# Tag comments can be prepended to the query:
|
||||
#
|
||||
@ -76,30 +76,6 @@ module QueryLogs
|
||||
thread_mattr_accessor :cached_comment, instance_accessor: false
|
||||
|
||||
class << self
|
||||
# Updates the context used to construct tags in the SQL comment.
|
||||
# If a block is given, it resets the provided keys to their
|
||||
# previous value once the block exits.
|
||||
def set_context(**options)
|
||||
options.symbolize_keys!
|
||||
|
||||
keys = options.keys
|
||||
previous_context = keys.zip(context.values_at(*keys)).to_h
|
||||
context.merge!(options)
|
||||
self.cached_comment = nil
|
||||
if block_given?
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
context.merge!(previous_context)
|
||||
self.cached_comment = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def clear_context # :nodoc:
|
||||
context.clear
|
||||
end
|
||||
|
||||
def call(sql) # :nodoc:
|
||||
if prepend_comment
|
||||
"#{self.comment} #{sql}"
|
||||
@ -108,6 +84,12 @@ def call(sql) # :nodoc:
|
||||
end.strip
|
||||
end
|
||||
|
||||
def clear_cache # :nodoc:
|
||||
self.cached_comment = nil
|
||||
end
|
||||
|
||||
ActiveSupport::ExecutionContext.after_change { ActiveRecord::QueryLogs.clear_cache }
|
||||
|
||||
private
|
||||
# Returns an SQL comment +String+ containing the query log tags.
|
||||
# Sets and returns a cached comment if <tt>cache_query_log_tags</tt> is +true+.
|
||||
@ -126,15 +108,13 @@ def uncached_comment
|
||||
end
|
||||
end
|
||||
|
||||
def context
|
||||
Thread.current[:active_record_query_log_tags_context] ||= {}
|
||||
end
|
||||
|
||||
def escape_sql_comment(content)
|
||||
content.to_s.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "")
|
||||
end
|
||||
|
||||
def tag_content
|
||||
context = ActiveSupport::ExecutionContext.to_h
|
||||
|
||||
tags.flat_map { |i| [*i] }.filter_map do |tag|
|
||||
key, handler = tag
|
||||
handler ||= taggings[key]
|
||||
|
@ -376,10 +376,6 @@ class Railtie < Rails::Railtie # :nodoc:
|
||||
if app.config.active_record.cache_query_log_tags
|
||||
ActiveRecord::QueryLogs.cache_query_log_tags = true
|
||||
end
|
||||
|
||||
app.reloader.before_class_unload { ActiveRecord::QueryLogs.clear_context }
|
||||
app.executor.to_run { ActiveRecord::QueryLogs.clear_context }
|
||||
app.executor.to_complete { ActiveRecord::QueryLogs.clear_context }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -332,7 +332,7 @@ db_namespace = namespace :db do
|
||||
|
||||
desc "Retrieves the current schema version number"
|
||||
task version: :load_config do
|
||||
puts "Current version: #{ActiveRecord::Base.connection.migration_context.current_version}"
|
||||
puts "Current version: #{ActiveRecord::Base.connection.schema_version}"
|
||||
end
|
||||
|
||||
# desc "Raises an error if there are pending migrations"
|
||||
|
@ -235,6 +235,9 @@ def check_validity_of_inverse!
|
||||
if has_inverse? && inverse_of.nil?
|
||||
raise InverseOfAssociationNotFoundError.new(self)
|
||||
end
|
||||
if has_inverse? && inverse_of == self
|
||||
raise InverseOfAssociationRecursiveError.new(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -632,6 +635,7 @@ def automatic_inverse_of
|
||||
# with the current reflection's klass name.
|
||||
def valid_inverse_reflection?(reflection)
|
||||
reflection &&
|
||||
reflection != self &&
|
||||
foreign_key == reflection.foreign_key &&
|
||||
klass <= reflection.active_record &&
|
||||
can_find_inverse_of_automatically?(reflection, true)
|
||||
|
@ -11,7 +11,7 @@ class Relation
|
||||
:reverse_order, :distinct, :create_with, :skip_query_cache]
|
||||
|
||||
CLAUSE_METHODS = [:where, :having, :from]
|
||||
INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having]
|
||||
INVALID_METHODS_FOR_DELETE_ALL = [:distinct]
|
||||
|
||||
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS
|
||||
|
||||
@ -485,8 +485,9 @@ def update_all(updates)
|
||||
arel = eager_loading? ? apply_join_dependency.arel : build_arel
|
||||
arel.source.left = table
|
||||
|
||||
stmt = arel.compile_update(values, table[primary_key])
|
||||
|
||||
group_values_arel_columns = arel_columns(group_values.uniq)
|
||||
having_clause_ast = having_clause.ast unless having_clause.empty?
|
||||
stmt = arel.compile_update(values, table[primary_key], having_clause_ast, group_values_arel_columns)
|
||||
klass.connection.update(stmt, "#{klass} Update All").tap { reset }
|
||||
end
|
||||
|
||||
@ -615,7 +616,9 @@ def delete_all
|
||||
arel = eager_loading? ? apply_join_dependency.arel : build_arel
|
||||
arel.source.left = table
|
||||
|
||||
stmt = arel.compile_delete(table[primary_key])
|
||||
group_values_arel_columns = arel_columns(group_values.uniq)
|
||||
having_clause_ast = having_clause.ast unless having_clause.empty?
|
||||
stmt = arel.compile_delete(table[primary_key], having_clause_ast, group_values_arel_columns)
|
||||
|
||||
klass.connection.delete(stmt, "#{klass} Delete All").tap { reset }
|
||||
end
|
||||
|
@ -14,7 +14,12 @@ def create_insert
|
||||
InsertManager.new
|
||||
end
|
||||
|
||||
def compile_update(values, key = nil)
|
||||
def compile_update(
|
||||
values,
|
||||
key = nil,
|
||||
having_clause = nil,
|
||||
group_values_columns = []
|
||||
)
|
||||
um = UpdateManager.new(source)
|
||||
um.set(values)
|
||||
um.take(limit)
|
||||
@ -22,16 +27,21 @@ def compile_update(values, key = nil)
|
||||
um.order(*orders)
|
||||
um.wheres = constraints
|
||||
um.key = key
|
||||
|
||||
um.group(group_values_columns) unless group_values_columns.empty?
|
||||
um.having(having_clause) unless having_clause.nil?
|
||||
um
|
||||
end
|
||||
|
||||
def compile_delete(key = nil)
|
||||
def compile_delete(key = nil, having_clause = nil, group_values_columns = [])
|
||||
dm = DeleteManager.new(source)
|
||||
dm.take(limit)
|
||||
dm.offset(offset)
|
||||
dm.order(*orders)
|
||||
dm.wheres = constraints
|
||||
dm.key = key
|
||||
dm.group(group_values_columns) unless group_values_columns.empty?
|
||||
dm.having(having_clause) unless having_clause.nil?
|
||||
dm
|
||||
end
|
||||
end
|
||||
|
@ -12,5 +12,21 @@ def from(relation)
|
||||
@ast.relation = relation
|
||||
self
|
||||
end
|
||||
|
||||
def group(columns)
|
||||
columns.each do |column|
|
||||
column = Nodes::SqlLiteral.new(column) if String === column
|
||||
column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column
|
||||
|
||||
@ast.groups.push Nodes::Group.new column
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def having(expr)
|
||||
@ast.havings << expr
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,12 +3,14 @@
|
||||
module Arel # :nodoc: all
|
||||
module Nodes
|
||||
class DeleteStatement < Arel::Nodes::Node
|
||||
attr_accessor :relation, :wheres, :orders, :limit, :offset, :key
|
||||
attr_accessor :relation, :wheres, :groups, :havings, :orders, :limit, :offset, :key
|
||||
|
||||
def initialize(relation = nil, wheres = [])
|
||||
super()
|
||||
@relation = relation
|
||||
@wheres = wheres
|
||||
@groups = []
|
||||
@havings = []
|
||||
@orders = []
|
||||
@limit = nil
|
||||
@offset = nil
|
||||
@ -30,6 +32,8 @@ def eql?(other)
|
||||
self.relation == other.relation &&
|
||||
self.wheres == other.wheres &&
|
||||
self.orders == other.orders &&
|
||||
self.groups == other.groups &&
|
||||
self.havings == other.havings &&
|
||||
self.limit == other.limit &&
|
||||
self.offset == other.offset &&
|
||||
self.key == other.key
|
||||
|
@ -3,13 +3,15 @@
|
||||
module Arel # :nodoc: all
|
||||
module Nodes
|
||||
class UpdateStatement < Arel::Nodes::Node
|
||||
attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key
|
||||
attr_accessor :relation, :wheres, :values, :groups, :havings, :orders, :limit, :offset, :key
|
||||
|
||||
def initialize(relation = nil)
|
||||
super()
|
||||
@relation = relation
|
||||
@wheres = []
|
||||
@values = []
|
||||
@groups = []
|
||||
@havings = []
|
||||
@orders = []
|
||||
@limit = nil
|
||||
@offset = nil
|
||||
@ -31,6 +33,8 @@ def eql?(other)
|
||||
self.relation == other.relation &&
|
||||
self.wheres == other.wheres &&
|
||||
self.values == other.values &&
|
||||
self.groups == other.groups &&
|
||||
self.havings == other.havings &&
|
||||
self.orders == other.orders &&
|
||||
self.limit == other.limit &&
|
||||
self.offset == other.offset &&
|
||||
|
@ -28,5 +28,21 @@ def set(values)
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def group(columns)
|
||||
columns.each do |column|
|
||||
column = Nodes::SqlLiteral.new(column) if String === column
|
||||
column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column
|
||||
|
||||
@ast.groups.push Nodes::Group.new column
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def having(expr)
|
||||
@ast.havings << expr
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -67,7 +67,8 @@ def visit_Arel_Nodes_NullsFirst(o, collector)
|
||||
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
|
||||
# these, we must use a subquery.
|
||||
def prepare_update_statement(o)
|
||||
if o.offset || has_join_sources?(o) && has_limit_or_offset_or_orders?(o)
|
||||
if o.offset || has_group_by_and_having?(o) ||
|
||||
has_join_sources?(o) && has_limit_or_offset_or_orders?(o)
|
||||
super
|
||||
else
|
||||
o
|
||||
|
@ -841,6 +841,10 @@ def has_limit_or_offset_or_orders?(o)
|
||||
o.limit || o.offset || !o.orders.empty?
|
||||
end
|
||||
|
||||
def has_group_by_and_having?(o)
|
||||
!o.groups.empty? && !o.havings.empty?
|
||||
end
|
||||
|
||||
# The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
|
||||
# on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in
|
||||
# an UPDATE statement, so in the MySQL visitor we redefine this to do that.
|
||||
@ -852,6 +856,8 @@ def prepare_update_statement(o)
|
||||
stmt.orders = []
|
||||
stmt.wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
|
||||
stmt.relation = o.relation.left if has_join_sources?(o)
|
||||
stmt.groups = o.groups unless o.groups.empty?
|
||||
stmt.havings = o.havings unless o.havings.empty?
|
||||
stmt
|
||||
else
|
||||
o
|
||||
@ -866,6 +872,8 @@ def build_subselect(key, o)
|
||||
core.froms = o.relation
|
||||
core.wheres = o.wheres
|
||||
core.projections = [key]
|
||||
core.groups = o.groups unless o.groups.empty?
|
||||
core.havings = o.havings unless o.havings.empty?
|
||||
stmt.limit = o.limit
|
||||
stmt.offset = o.offset
|
||||
stmt.orders = o.orders
|
||||
|
@ -213,11 +213,9 @@ def test_numeric_value_out_of_ranges_are_translated_to_specific_exception
|
||||
def test_exceptions_from_notifications_are_not_translated
|
||||
original_error = StandardError.new("This StandardError shouldn't get translated")
|
||||
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") { raise original_error }
|
||||
wrapped_error = assert_raises(ActiveSupport::Notifications::InstrumentationSubscriberError) do
|
||||
actual_error = assert_raises(StandardError) do
|
||||
@connection.execute("SELECT * FROM posts")
|
||||
end
|
||||
actual_error = wrapped_error.exceptions.first
|
||||
|
||||
assert_equal original_error, actual_error
|
||||
|
||||
ensure
|
||||
|
@ -49,7 +49,7 @@ def test_schema_define
|
||||
|
||||
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
|
||||
assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" }
|
||||
assert_equal 7, @connection.migration_context.current_version
|
||||
assert_equal 7, @connection.schema_version
|
||||
end
|
||||
|
||||
def test_schema_define_with_table_name_prefix
|
||||
|
@ -27,6 +27,8 @@
|
||||
statement1.orders = %w[x y z]
|
||||
statement1.limit = 42
|
||||
statement1.key = "zomg"
|
||||
statement1.groups = ["foo"]
|
||||
statement1.havings = []
|
||||
statement2 = Arel::Nodes::UpdateStatement.new
|
||||
statement2.relation = "zomg"
|
||||
statement2.wheres = 2
|
||||
@ -34,6 +36,8 @@
|
||||
statement2.orders = %w[x y z]
|
||||
statement2.limit = 42
|
||||
statement2.key = "zomg"
|
||||
statement2.groups = ["foo"]
|
||||
statement2.havings = []
|
||||
array = [statement1, statement2]
|
||||
assert_equal 1, array.uniq.size
|
||||
end
|
||||
|
@ -22,6 +22,57 @@ class UpdateManagerTest < Arel::Spec
|
||||
assert_match(/LIMIT 10/, um.to_sql)
|
||||
end
|
||||
|
||||
describe "having" do
|
||||
it "sets having" do
|
||||
users_table = Table.new(:users)
|
||||
posts_table = Table.new(:posts)
|
||||
join_source = Arel::Nodes::InnerJoin.new(users_table, posts_table)
|
||||
|
||||
update_manager = Arel::UpdateManager.new
|
||||
update_manager.table(join_source)
|
||||
update_manager.group(["posts.id"])
|
||||
update_manager.having("count(posts.id) >= 2")
|
||||
|
||||
assert_equal(["count(posts.id) >= 2"], update_manager.ast.havings)
|
||||
end
|
||||
end
|
||||
|
||||
describe "group" do
|
||||
it "adds columns to the AST when group value is a String" do
|
||||
users_table = Table.new(:users)
|
||||
posts_table = Table.new(:posts)
|
||||
join_source = Arel::Nodes::InnerJoin.new(users_table, posts_table)
|
||||
|
||||
update_manager = Arel::UpdateManager.new
|
||||
update_manager.table(join_source)
|
||||
update_manager.group(["posts.id"])
|
||||
update_manager.having("count(posts.id) >= 2")
|
||||
|
||||
assert_equal(1, update_manager.ast.groups.count)
|
||||
group_ast = update_manager.ast.groups.first
|
||||
_(group_ast).must_be_kind_of Nodes::Group
|
||||
assert_equal("posts.id", group_ast.expr)
|
||||
assert_equal(["count(posts.id) >= 2"], update_manager.ast.havings)
|
||||
end
|
||||
|
||||
it "adds columns to the AST when group value is a Symbol" do
|
||||
users_table = Table.new(:users)
|
||||
posts_table = Table.new(:posts)
|
||||
join_source = Arel::Nodes::InnerJoin.new(users_table, posts_table)
|
||||
|
||||
update_manager = Arel::UpdateManager.new
|
||||
update_manager.table(join_source)
|
||||
update_manager.group([:"posts.id"])
|
||||
update_manager.having("count(posts.id) >= 2")
|
||||
|
||||
assert_equal(1, update_manager.ast.groups.count)
|
||||
group_ast = update_manager.ast.groups.first
|
||||
_(group_ast).must_be_kind_of Nodes::Group
|
||||
assert_equal("posts.id", group_ast.expr)
|
||||
assert_equal(["count(posts.id) >= 2"], update_manager.ast.havings)
|
||||
end
|
||||
end
|
||||
|
||||
describe "set" do
|
||||
it "updates with null" do
|
||||
table = Table.new(:users)
|
||||
|
@ -25,6 +25,7 @@
|
||||
require "models/contract"
|
||||
require "models/subscription"
|
||||
require "models/book"
|
||||
require "models/branch"
|
||||
|
||||
class AutomaticInverseFindingTests < ActiveRecord::TestCase
|
||||
fixtures :ratings, :comments, :cars, :books
|
||||
@ -787,6 +788,30 @@ def test_with_hash_many_inversing_does_not_add_duplicate_associated_objects
|
||||
end
|
||||
end
|
||||
|
||||
def test_recursive_model_has_many_inversing
|
||||
with_has_many_inversing do
|
||||
main = Branch.create!
|
||||
feature = main.branches.create!
|
||||
topic = feature.branches.build
|
||||
|
||||
assert_equal(main, topic.branch.branch)
|
||||
end
|
||||
end
|
||||
|
||||
def test_recursive_inverse_on_recursive_model_has_many_inversing
|
||||
with_has_many_inversing do
|
||||
main = BrokenBranch.create!
|
||||
feature = main.branches.create!
|
||||
topic = feature.branches.build
|
||||
|
||||
error = assert_raises(ActiveRecord::InverseOfAssociationRecursiveError) do
|
||||
topic.branch.branch
|
||||
end
|
||||
|
||||
assert_equal("Inverse association branch (:branch in BrokenBranch) is recursive.", error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_unscope_does_not_set_inverse_when_incorrect
|
||||
interest = interests(:trainspotting)
|
||||
human = interest.human
|
||||
|
@ -428,6 +428,18 @@ def test_preload_makes_correct_number_of_queries_on_relation
|
||||
end
|
||||
end
|
||||
|
||||
def test_preload_for_hmt_with_conditions
|
||||
post = posts(:welcome)
|
||||
_normal_category = post.categories.create!(name: "Normal")
|
||||
special_category = post.special_categories.create!(name: "Special")
|
||||
|
||||
preloader = ActiveRecord::Associations::Preloader.new(records: [post], associations: :hmt_special_categories)
|
||||
preloader.call
|
||||
|
||||
assert_equal 1, post.hmt_special_categories.length
|
||||
assert_equal [special_category], post.hmt_special_categories
|
||||
end
|
||||
|
||||
def test_preload_groups_queries_with_same_scope
|
||||
book = books(:awdr)
|
||||
post = posts(:welcome)
|
||||
|
@ -72,4 +72,9 @@ class DelegatedTypeTest < ActiveRecord::TestCase
|
||||
assert_equal @uuid_entry_with_comment.entryable_uuid, @uuid_entry_with_comment.uuid_comment_uuid
|
||||
assert_nil @uuid_entry_with_comment.uuid_message_uuid
|
||||
end
|
||||
|
||||
test "builder method" do
|
||||
assert_respond_to Entry.new, :build_entryable
|
||||
assert_equal Message, Entry.new(entryable_type: "Message").build_entryable.class
|
||||
end
|
||||
end
|
||||
|
@ -11,6 +11,8 @@
|
||||
require "models/interest"
|
||||
require "models/owner"
|
||||
require "models/pet"
|
||||
require "models/entry"
|
||||
require "models/message"
|
||||
require "active_support/hash_with_indifferent_access"
|
||||
|
||||
class TestNestedAttributesInGeneral < ActiveRecord::TestCase
|
||||
@ -1117,3 +1119,15 @@ def test_extend_affects_nested_attributes
|
||||
assert_equal "from extension", pirate.treasures[0].name
|
||||
end
|
||||
end
|
||||
|
||||
class TestNestedAttributesForDelegatedType < ActiveRecord::TestCase
|
||||
setup do
|
||||
Entry.accepts_nested_attributes_for :entryable
|
||||
@entry = Entry.new(entryable_type: "Message", entryable_attributes: { subject: "Hello world!" })
|
||||
end
|
||||
|
||||
def test_should_build_a_new_record_based_on_the_delegated_type
|
||||
assert_not_predicate @entry.entryable, :persisted?
|
||||
assert_equal "Hello world!", @entry.entryable.subject
|
||||
end
|
||||
end
|
||||
|
@ -489,13 +489,11 @@ def test_query_cache_does_not_allow_sql_key_mutation
|
||||
payload[:sql].downcase!
|
||||
end
|
||||
|
||||
error = assert_raises ActiveSupport::Notifications::InstrumentationSubscriberError do
|
||||
assert_raises FrozenError do
|
||||
ActiveRecord::Base.cache do
|
||||
assert_queries(1) { Task.find(1); Task.find(1) }
|
||||
end
|
||||
end
|
||||
|
||||
assert error.exceptions.first.is_a?(FrozenError)
|
||||
ensure
|
||||
ActiveSupport::Notifications.unsubscribe subscriber
|
||||
end
|
||||
|
@ -11,13 +11,14 @@ class QueryLogsTest < ActiveRecord::TestCase
|
||||
}
|
||||
|
||||
def setup
|
||||
# QueryLogs context is automatically reset in Rails app via an executor hooks set in railtie
|
||||
# ActiveSupport::ExecutionContext context is automatically reset in Rails app via an executor hooks set in railtie
|
||||
# But not in Active Record's own test suite.
|
||||
ActiveRecord::QueryLogs.clear_context
|
||||
ActiveSupport::ExecutionContext.clear
|
||||
|
||||
# Enable the query tags logging
|
||||
@original_transformers = ActiveRecord.query_transformers
|
||||
@original_prepend = ActiveRecord::QueryLogs.prepend_comment
|
||||
@original_tags = ActiveRecord::QueryLogs.tags
|
||||
ActiveRecord.query_transformers += [ActiveRecord::QueryLogs]
|
||||
ActiveRecord::QueryLogs.prepend_comment = false
|
||||
ActiveRecord::QueryLogs.cache_query_log_tags = false
|
||||
@ -27,10 +28,13 @@ def setup
|
||||
def teardown
|
||||
ActiveRecord.query_transformers = @original_transformers
|
||||
ActiveRecord::QueryLogs.prepend_comment = @original_prepend
|
||||
ActiveRecord::QueryLogs.tags = []
|
||||
# QueryLogs context is automatically reset in Rails app via an executor hooks set in railtie
|
||||
ActiveRecord::QueryLogs.tags = @original_tags
|
||||
ActiveRecord::QueryLogs.prepend_comment = false
|
||||
ActiveRecord::QueryLogs.cache_query_log_tags = false
|
||||
ActiveRecord::QueryLogs.cached_comment = nil
|
||||
# ActiveSupport::ExecutionContext context is automatically reset in Rails app via an executor hooks set in railtie
|
||||
# But not in Active Record's own test suite.
|
||||
ActiveRecord::QueryLogs.clear_context
|
||||
ActiveSupport::ExecutionContext.clear
|
||||
end
|
||||
|
||||
def test_escaping_good_comment
|
||||
@ -57,8 +61,6 @@ def test_add_comments_to_beginning_of_query
|
||||
assert_sql(%r{/\*application:active_record\*/ select id from posts$}) do
|
||||
ActiveRecord::Base.connection.execute "select id from posts"
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::QueryLogs.prepend_comment = nil
|
||||
end
|
||||
|
||||
def test_exists_is_commented
|
||||
@ -112,41 +114,24 @@ def test_retrieves_comment_from_cache_when_enabled_and_set
|
||||
ActiveRecord::QueryLogs.stub(:cached_comment, "/*cached_comment*/") do
|
||||
assert_equal "/*cached_comment*/", ActiveRecord::QueryLogs.call("")
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::QueryLogs.cached_comment = nil
|
||||
ActiveRecord::QueryLogs.cache_query_log_tags = false
|
||||
end
|
||||
|
||||
def test_resets_cache_on_context_update
|
||||
ActiveRecord::QueryLogs.cache_query_log_tags = true
|
||||
ActiveRecord::QueryLogs.set_context(temporary: "value")
|
||||
ActiveSupport::ExecutionContext[:temporary] = "value"
|
||||
ActiveRecord::QueryLogs.tags = [ temporary_tag: ->(context) { context[:temporary] } ]
|
||||
|
||||
assert_equal "/*temporary_tag:value*/", ActiveRecord::QueryLogs.call("")
|
||||
|
||||
ActiveRecord::QueryLogs.set_context(temporary: "new_value")
|
||||
ActiveSupport::ExecutionContext[:temporary] = "new_value"
|
||||
|
||||
assert_nil ActiveRecord::QueryLogs.cached_comment
|
||||
assert_equal "/*temporary_tag:new_value*/", ActiveRecord::QueryLogs.call("")
|
||||
ensure
|
||||
ActiveRecord::QueryLogs.cached_comment = nil
|
||||
ActiveRecord::QueryLogs.cache_query_log_tags = false
|
||||
end
|
||||
|
||||
def test_ensure_context_has_symbol_keys
|
||||
ActiveRecord::QueryLogs.tags = [ new_key: ->(context) { context[:symbol_key] } ]
|
||||
ActiveRecord::QueryLogs.set_context("symbol_key" => "symbolized")
|
||||
|
||||
assert_sql(%r{/\*new_key:symbolized}) do
|
||||
Dashboard.first
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::QueryLogs.set_context(application_name: nil)
|
||||
end
|
||||
|
||||
def test_default_tag_behavior
|
||||
ActiveRecord::QueryLogs.tags = [:application, :foo]
|
||||
ActiveRecord::QueryLogs.set_context(foo: "bar") do
|
||||
ActiveSupport::ExecutionContext.set(foo: "bar") do
|
||||
assert_sql(%r{/\*application:active_record,foo:bar\*/}) do
|
||||
Dashboard.first
|
||||
end
|
||||
@ -157,39 +142,29 @@ def test_default_tag_behavior
|
||||
end
|
||||
|
||||
def test_empty_comments_are_not_added
|
||||
original_tags = ActiveRecord::QueryLogs.tags
|
||||
ActiveRecord::QueryLogs.tags = [ empty: -> { nil } ]
|
||||
assert_sql(%r{select id from posts$}) do
|
||||
ActiveRecord::Base.connection.execute "select id from posts"
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::QueryLogs.tags = original_tags
|
||||
end
|
||||
|
||||
def test_custom_basic_tags
|
||||
original_tags = ActiveRecord::QueryLogs.tags
|
||||
ActiveRecord::QueryLogs.tags = [ :application, { custom_string: "test content" } ]
|
||||
|
||||
assert_sql(%r{/\*application:active_record,custom_string:test content\*/$}) do
|
||||
Dashboard.first
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::QueryLogs.tags = original_tags
|
||||
end
|
||||
|
||||
def test_custom_proc_tags
|
||||
original_tags = ActiveRecord::QueryLogs.tags
|
||||
ActiveRecord::QueryLogs.tags = [ :application, { custom_proc: -> { "test content" } } ]
|
||||
|
||||
assert_sql(%r{/\*application:active_record,custom_proc:test content\*/$}) do
|
||||
Dashboard.first
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::QueryLogs.tags = original_tags
|
||||
end
|
||||
|
||||
def test_multiple_custom_tags
|
||||
original_tags = ActiveRecord::QueryLogs.tags
|
||||
ActiveRecord::QueryLogs.tags = [
|
||||
:application,
|
||||
{ custom_proc: -> { "test content" }, another_proc: -> { "more test content" } },
|
||||
@ -198,34 +173,14 @@ def test_multiple_custom_tags
|
||||
assert_sql(%r{/\*application:active_record,custom_proc:test content,another_proc:more test content\*/$}) do
|
||||
Dashboard.first
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::QueryLogs.tags = original_tags
|
||||
end
|
||||
|
||||
def test_custom_proc_context_tags
|
||||
original_tags = ActiveRecord::QueryLogs.tags
|
||||
ActiveRecord::QueryLogs.set_context(foo: "bar")
|
||||
ActiveSupport::ExecutionContext[:foo] = "bar"
|
||||
ActiveRecord::QueryLogs.tags = [ :application, { custom_context_proc: ->(context) { context[:foo] } } ]
|
||||
|
||||
assert_sql(%r{/\*application:active_record,custom_context_proc:bar\*/$}) do
|
||||
Dashboard.first
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::QueryLogs.set_context(foo: nil)
|
||||
ActiveRecord::QueryLogs.tags = original_tags
|
||||
end
|
||||
|
||||
def test_set_context_restore_state
|
||||
original_tags = ActiveRecord::QueryLogs.tags
|
||||
ActiveRecord::QueryLogs.tags = [foo: ->(context) { context[:foo] }]
|
||||
ActiveRecord::QueryLogs.set_context(foo: "bar") do
|
||||
assert_sql(%r{/\*foo:bar\*/$}) { Dashboard.first }
|
||||
ActiveRecord::QueryLogs.set_context(foo: "plop") do
|
||||
assert_sql(%r{/\*foo:plop\*/$}) { Dashboard.first }
|
||||
end
|
||||
assert_sql(%r{/\*foo:bar\*/$}) { Dashboard.first }
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::QueryLogs.tags = original_tags
|
||||
end
|
||||
end
|
||||
|
@ -5,9 +5,10 @@
|
||||
require "models/post"
|
||||
require "models/pet"
|
||||
require "models/toy"
|
||||
require "models/comment"
|
||||
|
||||
class DeleteAllTest < ActiveRecord::TestCase
|
||||
fixtures :authors, :author_addresses, :posts, :pets, :toys
|
||||
fixtures :authors, :author_addresses, :comments, :posts, :pets, :toys
|
||||
|
||||
def test_destroy_all
|
||||
davids = Author.where(name: "David")
|
||||
@ -53,10 +54,22 @@ def test_delete_all_loaded
|
||||
assert_predicate davids, :loaded?
|
||||
end
|
||||
|
||||
def test_delete_all_with_group_by_and_having
|
||||
minimum_comments_count = 2
|
||||
posts_to_be_deleted = Post.most_commented(minimum_comments_count).all.to_a
|
||||
assert_operator posts_to_be_deleted.length, :>, 0
|
||||
|
||||
assert_difference("Post.count", -posts_to_be_deleted.length) do
|
||||
Post.most_commented(minimum_comments_count).delete_all
|
||||
end
|
||||
|
||||
posts_to_be_deleted.each do |deleted_post|
|
||||
assert_raise(ActiveRecord::RecordNotFound) { deleted_post.reload }
|
||||
end
|
||||
end
|
||||
|
||||
def test_delete_all_with_unpermitted_relation_raises_error
|
||||
assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all }
|
||||
assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
|
||||
assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all }
|
||||
end
|
||||
|
||||
def test_delete_all_with_joins_and_where_part_is_hash
|
||||
|
@ -44,6 +44,20 @@ def test_update_all_with_blank_argument
|
||||
assert_equal "Empty list of attributes to change", error.message
|
||||
end
|
||||
|
||||
def test_update_all_with_group_by
|
||||
minimum_comments_count = 2
|
||||
|
||||
Post.most_commented(minimum_comments_count).update_all(title: "ig")
|
||||
posts = Post.most_commented(minimum_comments_count).all.to_a
|
||||
|
||||
assert_operator posts.length, :>, 0
|
||||
assert posts.all? { |post| post.comments.length >= minimum_comments_count }
|
||||
assert posts.all? { |post| "ig" == post.title }
|
||||
|
||||
post = Post.joins(:comments).group("posts.id").having("count(comments.id) < #{minimum_comments_count}").first
|
||||
assert_not_equal "ig", post.title
|
||||
end
|
||||
|
||||
def test_update_all_with_joins
|
||||
pets = Pet.joins(:toys).where(toys: { name: "Bone" })
|
||||
|
||||
|
11
activerecord/test/models/branch.rb
Normal file
11
activerecord/test/models/branch.rb
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Branch < ActiveRecord::Base
|
||||
has_many :branches
|
||||
belongs_to :branch, optional: true
|
||||
end
|
||||
|
||||
class BrokenBranch < Branch
|
||||
has_many :branches, class_name: "BrokenBranch", foreign_key: :branch_id
|
||||
belongs_to :branch, optional: true, inverse_of: :branch, class_name: "BrokenBranch"
|
||||
end
|
@ -34,6 +34,11 @@ def greeting
|
||||
|
||||
scope :limit_by, lambda { |l| limit(l) }
|
||||
scope :locked, -> { lock }
|
||||
scope :most_commented, lambda { |comments_count|
|
||||
joins(:comments)
|
||||
.group("posts.id")
|
||||
.having("count(comments.id) >= #{comments_count}")
|
||||
}
|
||||
|
||||
belongs_to :author
|
||||
belongs_to :readonly_author, -> { readonly }, class_name: "Author", foreign_key: :author_id
|
||||
@ -109,6 +114,7 @@ def greeting
|
||||
|
||||
has_many :category_posts, class_name: "CategoryPost"
|
||||
has_many :scategories, through: :category_posts, source: :category
|
||||
has_many :hmt_special_categories, -> { where.not(name: nil) }, through: :category_posts, source: :category, class_name: "SpecialCategory"
|
||||
has_and_belongs_to_many :categories
|
||||
has_and_belongs_to_many :special_categories, join_table: "categories_posts", association_foreign_key: "category_id"
|
||||
|
||||
|
@ -145,6 +145,10 @@
|
||||
t.boolean :has_fun, null: false, default: false
|
||||
end
|
||||
|
||||
create_table :branches, force: true do |t|
|
||||
t.references :branch
|
||||
end
|
||||
|
||||
create_table :bulbs, primary_key: "ID", force: true do |t|
|
||||
t.integer :car_id
|
||||
t.string :name
|
||||
|
@ -34,11 +34,13 @@ module ActiveSupport
|
||||
extend ActiveSupport::Autoload
|
||||
|
||||
autoload :Concern
|
||||
autoload :CodeGenerator
|
||||
autoload :ActionableError
|
||||
autoload :ConfigurationFile
|
||||
autoload :CurrentAttributes
|
||||
autoload :Dependencies
|
||||
autoload :DescendantsTracker
|
||||
autoload :ExecutionContext
|
||||
autoload :ExecutionWrapper
|
||||
autoload :Executor
|
||||
autoload :FileUpdateChecker
|
||||
|
65
activesupport/lib/active_support/code_generator.rb
Normal file
65
activesupport/lib/active_support/code_generator.rb
Normal file
@ -0,0 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ActiveSupport
|
||||
class CodeGenerator # :nodoc:
|
||||
class MethodSet
|
||||
METHOD_CACHES = Hash.new { |h, k| h[k] = Module.new }
|
||||
|
||||
def initialize(namespace)
|
||||
@cache = METHOD_CACHES[namespace]
|
||||
@sources = []
|
||||
@methods = {}
|
||||
end
|
||||
|
||||
def define_cached_method(name, as: name)
|
||||
name = name.to_sym
|
||||
as = as.to_sym
|
||||
@methods.fetch(name) do
|
||||
unless @cache.method_defined?(as)
|
||||
yield @sources
|
||||
end
|
||||
@methods[name] = as
|
||||
end
|
||||
end
|
||||
|
||||
def apply(owner, path, line)
|
||||
unless @sources.empty?
|
||||
@cache.module_eval("# frozen_string_literal: true\n" + @sources.join(";"), path, line)
|
||||
end
|
||||
@methods.each do |name, as|
|
||||
owner.define_method(name, @cache.instance_method(as))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def batch(owner, path, line)
|
||||
if owner.is_a?(CodeGenerator)
|
||||
yield owner
|
||||
else
|
||||
instance = new(owner, path, line)
|
||||
result = yield instance
|
||||
instance.execute
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(owner, path, line)
|
||||
@owner = owner
|
||||
@path = path
|
||||
@line = line
|
||||
@namespaces = Hash.new { |h, k| h[k] = MethodSet.new(k) }
|
||||
end
|
||||
|
||||
def define_cached_method(name, namespace:, as: name, &block)
|
||||
@namespaces[namespace].define_cached_method(name, as: as, &block)
|
||||
end
|
||||
|
||||
def execute
|
||||
@namespaces.each_value do |method_set|
|
||||
method_set.apply(@owner, @path, @line - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,9 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/digest/uuid"
|
||||
|
||||
module ActiveSupport
|
||||
class << self
|
||||
delegate :use_rfc4122_namespaced_uuids, :use_rfc4122_namespaced_uuids=, to: :'Digest::UUID'
|
||||
end
|
||||
end
|
||||
|
@ -98,25 +98,37 @@ def instance
|
||||
|
||||
# Declares one or more attributes that will be given both class and instance accessor methods.
|
||||
def attribute(*names)
|
||||
generated_attribute_methods.module_eval do
|
||||
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
|
||||
names.each do |name|
|
||||
define_method(name) do
|
||||
attributes[name.to_sym]
|
||||
owner.define_cached_method(name, namespace: :current_attributes) do |batch|
|
||||
batch <<
|
||||
"def #{name}" <<
|
||||
"attributes[:#{name}]" <<
|
||||
"end"
|
||||
end
|
||||
|
||||
define_method("#{name}=") do |attribute|
|
||||
attributes[name.to_sym] = attribute
|
||||
owner.define_cached_method("#{name}=", namespace: :current_attributes) do |batch|
|
||||
batch <<
|
||||
"def #{name}=(value)" <<
|
||||
"attributes[:#{name}] = value" <<
|
||||
"end"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
names.each do |name|
|
||||
define_singleton_method(name) do
|
||||
instance.public_send(name)
|
||||
end
|
||||
|
||||
define_singleton_method("#{name}=") do |attribute|
|
||||
instance.public_send("#{name}=", attribute)
|
||||
ActiveSupport::CodeGenerator.batch(singleton_class, __FILE__, __LINE__) do |owner|
|
||||
names.each do |name|
|
||||
owner.define_cached_method(name, namespace: :current_attributes_delegation) do |batch|
|
||||
batch <<
|
||||
"def #{name}" <<
|
||||
"instance.#{name}" <<
|
||||
"end"
|
||||
end
|
||||
owner.define_cached_method("#{name}=", namespace: :current_attributes_delegation) do |batch|
|
||||
batch <<
|
||||
"def #{name}=(value)" <<
|
||||
"instance.#{name} = value" <<
|
||||
"end"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
53
activesupport/lib/active_support/execution_context.rb
Normal file
53
activesupport/lib/active_support/execution_context.rb
Normal file
@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ActiveSupport
|
||||
module ExecutionContext # :nodoc:
|
||||
@after_change_callbacks = []
|
||||
class << self
|
||||
def after_change(&block)
|
||||
@after_change_callbacks << block
|
||||
end
|
||||
|
||||
# Updates the execution context. If a block is given, it resets the provided keys to their
|
||||
# previous value once the block exits.
|
||||
def set(**options)
|
||||
options.symbolize_keys!
|
||||
keys = options.keys
|
||||
|
||||
store = self.store
|
||||
|
||||
previous_context = keys.zip(store.values_at(*keys)).to_h
|
||||
|
||||
store.merge!(options)
|
||||
@after_change_callbacks.each(&:call)
|
||||
|
||||
if block_given?
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
store.merge!(previous_context)
|
||||
@after_change_callbacks.each(&:call)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
store[key.to_sym] = value
|
||||
@after_change_callbacks.each(&:call)
|
||||
end
|
||||
|
||||
def to_h
|
||||
store.dup
|
||||
end
|
||||
|
||||
def clear
|
||||
store.clear
|
||||
end
|
||||
|
||||
private
|
||||
def store
|
||||
Thread.current[:active_support_execution_context] ||= {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ActiveSupport::ExecutionContext::TestHelper # :nodoc:
|
||||
def before_setup
|
||||
ActiveSupport::ExecutionContext.clear
|
||||
super
|
||||
end
|
||||
|
||||
def after_teardown
|
||||
super
|
||||
ActiveSupport::ExecutionContext.clear
|
||||
end
|
||||
end
|
@ -93,8 +93,16 @@ def iterate_guarding_exceptions(listeners)
|
||||
exceptions ||= []
|
||||
exceptions << e
|
||||
end
|
||||
ensure
|
||||
raise InstrumentationSubscriberError.new(exceptions) unless exceptions.nil?
|
||||
|
||||
if exceptions
|
||||
if exceptions.size == 1
|
||||
raise exceptions.first
|
||||
else
|
||||
raise InstrumentationSubscriberError.new(exceptions), cause: exceptions.first
|
||||
end
|
||||
end
|
||||
|
||||
listeners
|
||||
end
|
||||
|
||||
def listeners_for(name)
|
||||
|
@ -27,6 +27,12 @@ class Railtie < Rails::Railtie # :nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
initializer "active_support.reset_execution_context" do |app|
|
||||
app.reloader.before_class_unload { ActiveSupport::ExecutionContext.clear }
|
||||
app.executor.to_run { ActiveSupport::ExecutionContext.clear }
|
||||
app.executor.to_complete { ActiveSupport::ExecutionContext.clear }
|
||||
end
|
||||
|
||||
initializer "active_support.reset_all_current_attributes_instances" do |app|
|
||||
executor_around_test_case = app.config.active_support.delete(:executor_around_test_case)
|
||||
|
||||
@ -41,6 +47,9 @@ class Railtie < Rails::Railtie # :nodoc:
|
||||
else
|
||||
require "active_support/current_attributes/test_helper"
|
||||
include ActiveSupport::CurrentAttributes::TestHelper
|
||||
|
||||
require "active_support/execution_context/test_helper"
|
||||
include ActiveSupport::ExecutionContext::TestHelper
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -133,7 +142,7 @@ class Railtie < Rails::Railtie # :nodoc:
|
||||
config.after_initialize do
|
||||
if app.config.active_support.use_rfc4122_namespaced_uuids
|
||||
require "active_support/core_ext/digest"
|
||||
ActiveSupport.use_rfc4122_namespaced_uuids = app.config.active_support.use_rfc4122_namespaced_uuids
|
||||
::Digest::UUID.use_rfc4122_namespaced_uuids = app.config.active_support.use_rfc4122_namespaced_uuids
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -4,14 +4,6 @@
|
||||
require "active_support/core_ext/digest"
|
||||
|
||||
class DigestUUIDExt < ActiveSupport::TestCase
|
||||
def with_use_rfc4122_namespaced_uuids_set
|
||||
old_value = ActiveSupport.use_rfc4122_namespaced_uuids
|
||||
ActiveSupport.use_rfc4122_namespaced_uuids = true
|
||||
yield
|
||||
ensure
|
||||
ActiveSupport.use_rfc4122_namespaced_uuids = old_value
|
||||
end
|
||||
|
||||
def test_constants
|
||||
assert_equal "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "%08x-%04x-%04x-%04x-%04x%08x" % Digest::UUID::DNS_NAMESPACE.unpack("NnnnnN")
|
||||
assert_equal "6ba7b811-9dad-11d1-80b4-00c04fd430c8", "%08x-%04x-%04x-%04x-%04x%08x" % Digest::UUID::URL_NAMESPACE.unpack("NnnnnN")
|
||||
@ -184,4 +176,13 @@ def test_invalid_hash_class
|
||||
Digest::UUID.uuid_from_hash(OpenSSL::Digest::SHA256, Digest::UUID::OID_NAMESPACE, "1.2.3")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def with_use_rfc4122_namespaced_uuids_set
|
||||
old_value = Digest::UUID.use_rfc4122_namespaced_uuids
|
||||
Digest::UUID.use_rfc4122_namespaced_uuids = true
|
||||
yield
|
||||
ensure
|
||||
Digest::UUID.use_rfc4122_namespaced_uuids = old_value
|
||||
end
|
||||
end
|
||||
|
@ -1,17 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "abstract_unit"
|
||||
require "active_support/current_attributes/test_helper"
|
||||
|
||||
class CurrentAttributesTest < ActiveSupport::TestCase
|
||||
# CurrentAttributes is automatically reset in Rails app via executor hooks set in railtie
|
||||
# But not in Active Support's own test suite.
|
||||
setup do
|
||||
ActiveSupport::CurrentAttributes.reset_all
|
||||
end
|
||||
|
||||
teardown do
|
||||
ActiveSupport::CurrentAttributes.reset_all
|
||||
end
|
||||
include ActiveSupport::CurrentAttributes::TestHelper
|
||||
|
||||
Person = Struct.new(:id, :name, :time_zone)
|
||||
|
||||
|
47
activesupport/test/execution_context_test.rb
Normal file
47
activesupport/test/execution_context_test.rb
Normal file
@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "abstract_unit"
|
||||
require "active_support/execution_context/test_helper"
|
||||
|
||||
class ExecutionContextTest < ActiveSupport::TestCase
|
||||
# ExecutionContext is automatically reset in Rails app via executor hooks set in railtie
|
||||
# But not in Active Support's own test suite.
|
||||
include ActiveSupport::ExecutionContext::TestHelper
|
||||
|
||||
test "#set restore the modified keys when the block exits" do
|
||||
assert_nil ActiveSupport::ExecutionContext.to_h[:foo]
|
||||
ActiveSupport::ExecutionContext.set(foo: "bar") do
|
||||
assert_equal "bar", ActiveSupport::ExecutionContext.to_h[:foo]
|
||||
ActiveSupport::ExecutionContext.set(foo: "plop") do
|
||||
assert_equal "plop", ActiveSupport::ExecutionContext.to_h[:foo]
|
||||
end
|
||||
assert_equal "bar", ActiveSupport::ExecutionContext.to_h[:foo]
|
||||
|
||||
ActiveSupport::ExecutionContext[:direct_assignment] = "present"
|
||||
ActiveSupport::ExecutionContext.set(multi_assignment: "present")
|
||||
end
|
||||
|
||||
assert_nil ActiveSupport::ExecutionContext.to_h[:foo]
|
||||
|
||||
assert_equal "present", ActiveSupport::ExecutionContext.to_h[:direct_assignment]
|
||||
assert_equal "present", ActiveSupport::ExecutionContext.to_h[:multi_assignment]
|
||||
end
|
||||
|
||||
test "#set coerce keys to symbol" do
|
||||
ActiveSupport::ExecutionContext.set("foo" => "bar") do
|
||||
assert_equal "bar", ActiveSupport::ExecutionContext.to_h[:foo]
|
||||
end
|
||||
end
|
||||
|
||||
test "#[]= coerce keys to symbol" do
|
||||
ActiveSupport::ExecutionContext["symbol_key"] = "symbolized"
|
||||
assert_equal "symbolized", ActiveSupport::ExecutionContext.to_h[:symbol_key]
|
||||
end
|
||||
|
||||
test "#to_h returns a copy of the context" do
|
||||
ActiveSupport::ExecutionContext[:foo] = 42
|
||||
context = ActiveSupport::ExecutionContext.to_h
|
||||
context[:foo] = 43
|
||||
assert_equal 42, ActiveSupport::ExecutionContext.to_h[:foo]
|
||||
end
|
||||
end
|
@ -92,18 +92,23 @@ def test_listen_to_everything
|
||||
], listener.events
|
||||
end
|
||||
|
||||
def test_listen_start_exception_consistency
|
||||
def test_listen_start_multiple_exception_consistency
|
||||
notifier = Fanout.new
|
||||
listener = Listener.new
|
||||
notifier.subscribe nil, BadStartListener.new
|
||||
notifier.subscribe nil, BadStartListener.new
|
||||
notifier.subscribe nil, listener
|
||||
|
||||
assert_raises InstrumentationSubscriberError do
|
||||
error = assert_raises InstrumentationSubscriberError do
|
||||
notifier.start "hello", 1, {}
|
||||
end
|
||||
assert_raises InstrumentationSubscriberError do
|
||||
assert_instance_of BadListenerException, error.cause
|
||||
|
||||
error = assert_raises InstrumentationSubscriberError do
|
||||
notifier.start "world", 1, {}
|
||||
end
|
||||
assert_instance_of BadListenerException, error.cause
|
||||
|
||||
notifier.finish "world", 1, {}
|
||||
notifier.finish "hello", 1, {}
|
||||
|
||||
@ -116,20 +121,24 @@ def test_listen_start_exception_consistency
|
||||
], listener.events
|
||||
end
|
||||
|
||||
def test_listen_finish_exception_consistency
|
||||
def test_listen_finish_multiple_exception_consistency
|
||||
notifier = Fanout.new
|
||||
listener = Listener.new
|
||||
notifier.subscribe nil, BadFinishListener.new
|
||||
notifier.subscribe nil, BadFinishListener.new
|
||||
notifier.subscribe nil, listener
|
||||
|
||||
notifier.start "hello", 1, {}
|
||||
notifier.start "world", 1, {}
|
||||
assert_raises InstrumentationSubscriberError do
|
||||
error = assert_raises InstrumentationSubscriberError do
|
||||
notifier.finish "world", 1, {}
|
||||
end
|
||||
assert_raises InstrumentationSubscriberError do
|
||||
assert_instance_of BadListenerException, error.cause
|
||||
|
||||
error = assert_raises InstrumentationSubscriberError do
|
||||
notifier.finish "hello", 1, {}
|
||||
end
|
||||
assert_instance_of BadListenerException, error.cause
|
||||
|
||||
assert_equal 4, listener.events.length
|
||||
assert_equal [
|
||||
|
@ -26,7 +26,7 @@ namespace :guides do
|
||||
|
||||
# Validate guides -------------------------------------------------------------------------
|
||||
desc 'Validate guides, use ONLY=foo to process just "foo.html"'
|
||||
task validate: :encoding do
|
||||
task :validate do
|
||||
ruby "w3c_validator.rb"
|
||||
end
|
||||
|
||||
|
@ -390,6 +390,14 @@ def welcome_email
|
||||
end
|
||||
```
|
||||
|
||||
The same technique works to specify a sender name:
|
||||
|
||||
```ruby
|
||||
class UserMailer < ApplicationMailer
|
||||
default from: email_address_with_name('notification@example.com', 'Example Company Notifications')
|
||||
end
|
||||
```
|
||||
|
||||
If the name is a blank string, it returns just the address.
|
||||
|
||||
[`email_address_with_name`]: https://api.rubyonrails.org/classes/ActionMailer/Base.html#method-i-email_address_with_name
|
||||
@ -810,6 +818,8 @@ files (environment.rb, production.rb, etc...)
|
||||
|`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](https://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.|
|
||||
|`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing. If this value is `false`, `deliveries` array will not be populated even if `delivery_method` is `:test`.|
|
||||
|`deliveries`|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.|
|
||||
|`delivery_job`|The job class used with `deliver_later`. Defaults to `ActionMailer::MailDeliveryJob`.|
|
||||
|`deliver_later_queue_name`|The name of the queue used with `deliver_later`.|
|
||||
|`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).|
|
||||
|
||||
For a complete writeup of possible configurations see the
|
||||
|
@ -80,7 +80,7 @@ Rails supports six types of associations:
|
||||
* [`has_one :through`][`has_one`]
|
||||
* [`has_and_belongs_to_many`][]
|
||||
|
||||
Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain [Primary Key](https://en.wikipedia.org/wiki/Unique_key)-[Foreign Key](https://en.wikipedia.org/wiki/Foreign_key) information between instances of the two models, and you also get a number of utility methods added to your model.
|
||||
Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain [Primary Key](https://en.wikipedia.org/wiki/Primary_key)-[Foreign Key](https://en.wikipedia.org/wiki/Foreign_key) information between instances of the two models, and you also get a number of utility methods added to your model.
|
||||
|
||||
In the remainder of this guide, you'll learn how to declare and use the various forms of associations. But first, a quick introduction to the situations where each association type is appropriate.
|
||||
|
||||
|
@ -456,6 +456,27 @@ However, if an engine supports Rails 6 or Rails 6.1 and does not control its par
|
||||
|
||||
3. In `classic` mode, a file `app/model/concerns/foo.rb` is allowed to define both `Foo` and `Concerns::Foo`. In `zeitwerk` mode, there's only one option: it has to define `Foo`. In order to be compatible, define `Foo`.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
### Manual Testing
|
||||
|
||||
The task `zeitwerk:check` checks if the project tree follows the expected naming conventions and it is handy for manual checks. For example, if you're migrating from `classic` to `zeitwerk` mode, or if you're fixing something:
|
||||
|
||||
```
|
||||
% bin/rails zeitwerk:check
|
||||
Hold on, I am eager loading the application.
|
||||
All is good!
|
||||
```
|
||||
|
||||
There can be additional output depending on the application configuration, but the last "All is good!" is what you are looking for.
|
||||
|
||||
### Automated Testing
|
||||
|
||||
It is a good practice to verify in the test suite that the project eager loads correctly.
|
||||
|
||||
That covers Zeitwerk naming compliance and other possible error conditions. Please check the [section about testing eager loading](testing.html#testing-eager-loading) in the [_Testing Rails Applications_](testing.html) guide.
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
|
@ -29,9 +29,9 @@ Starting with Rails 6, Rails ships with a new and better way to autoload, which
|
||||
Why Switch from `classic` to `zeitwerk`?
|
||||
----------------------------------------
|
||||
|
||||
The `classic` autoloader has been extremely useful, but had a number of [issues](https://guides.rubyonrails.org/v6.1/autoloading_and_reloading_constants_classic_mode.html#common-gotchas) that made autoloading a bit tricky and confusing at times. Zeitwerk was developed to address them, among other [motivations](https://github.com/fxn/zeitwerk#motivation).
|
||||
The `classic` autoloader has been extremely useful, but had a number of [issues](https://guides.rubyonrails.org/v6.1/autoloading_and_reloading_constants_classic_mode.html#common-gotchas) that made autoloading a bit tricky and confusing at times. Zeitwerk was developed to address this, among other [motivations](https://github.com/fxn/zeitwerk#motivation).
|
||||
|
||||
When upgrading to Rails 6.x, it is highly encouraged to switch to `zeitwerk` mode because `classic` mode is deprecated.
|
||||
When upgrading to Rails 6.x, it is highly encouraged to switch to `zeitwerk` mode because it is a better autoloader, `classic` mode is deprecated.
|
||||
|
||||
Rails 7 ends the transition period and does not include `classic` mode.
|
||||
|
||||
@ -80,7 +80,7 @@ config.autoloader = :zeitwerk
|
||||
|
||||
In Rails 7 there is only `zeitwerk` mode, you do not need to do anything to enable it.
|
||||
|
||||
Indeed, the setter `config.autoloader=` does not even exist. If `config/application.rb` has it, please just delete the line.
|
||||
Indeed, in Rails 7 the setter `config.autoloader=` does not even exist. If `config/application.rb` uses it, please delete the line.
|
||||
|
||||
|
||||
How to Verify The Application Runs in `zeitwerk` Mode?
|
||||
@ -162,6 +162,8 @@ Hold on, I am eager loading the application.
|
||||
All is good!
|
||||
```
|
||||
|
||||
Once all is good, it is recommended to keep validating the project in the test suite. The section [_Check Zeitwerk Compliance in the Test Suite_](#check-zeitwerk-compliance-in-the-test-suite) explains how to do this.
|
||||
|
||||
### Concerns
|
||||
|
||||
You can autoload and eager load from a standard structure with `concerns` subdirectories like
|
||||
@ -306,11 +308,26 @@ Please make sure to depend on at least Bootsnap 1.4.4.
|
||||
Check Zeitwerk Compliance in the Test Suite
|
||||
-------------------------------------------
|
||||
|
||||
The Rake task `zeitwerk:check` just eager loads, because doing so triggers built-in validations in Zeitwerk.
|
||||
The task `zeitwerk:check` is handy while migrating. Once the project is compliant, it is recommended to automate this check. In order to do so, it is enough to eager load the application, which is all `zeitwerk:check` does, indeed.
|
||||
|
||||
You can add the equivalent of this to your test suite to make sure the application always loads correctly regardless of test coverage:
|
||||
### Continuous Integration
|
||||
|
||||
### minitest
|
||||
If your project has continuous integration in place, it is a good idea to eager load the application when the suite runs there. If the application cannot be eager loaded for whatever reason, you want to know in CI, better than in production, right?
|
||||
|
||||
CIs typically set some environment variable to indicate the test suite is running there. For example, it could be `CI`:
|
||||
|
||||
```ruby
|
||||
# config/environments/test.rb
|
||||
config.eager_load = ENV["CI"].present?
|
||||
```
|
||||
|
||||
Starting with Rails 7, newly generated applications are configured that way by default.
|
||||
|
||||
### Bare Test Suites
|
||||
|
||||
If your project does not have continuous integration, you can still eager load in the test suite by calling `Rails.application.eager_load!`:
|
||||
|
||||
#### minitest
|
||||
|
||||
```ruby
|
||||
require "test_helper"
|
||||
@ -322,7 +339,7 @@ class ZeitwerkComplianceTest < ActiveSupport::TestCase
|
||||
end
|
||||
```
|
||||
|
||||
### RSpec
|
||||
#### RSpec
|
||||
|
||||
```ruby
|
||||
require "rails_helper"
|
||||
@ -334,6 +351,21 @@ RSpec.describe "Zeitwerk compliance" do
|
||||
end
|
||||
```
|
||||
|
||||
Delete any `require` calls
|
||||
--------------------------
|
||||
|
||||
In my experience, projects generally do not do this. But I've seen a couple, and have heard of a few others.
|
||||
|
||||
In Rails application you use `require` exclusively to load code from `lib` or from 3rd party like gem dependencies or the standard library. **Never load autoloadable application code with `require`**. See why this was a bad idea already in `classic` [here](https://guides.rubyonrails.org/v6.1/autoloading_and_reloading_constants_classic_mode.html#autoloading-and-require).
|
||||
|
||||
```ruby
|
||||
require "nokogiri" # GOOD
|
||||
require "net/http" # GOOD
|
||||
require "user" # BAD, DELETE THIS (assuming app/models/user.rb)
|
||||
```
|
||||
|
||||
Please delete any `require` calls of that type.
|
||||
|
||||
New Features You Can Leverage
|
||||
-----------------------------
|
||||
|
||||
@ -343,13 +375,12 @@ All known use cases of `require_dependency` have been eliminated with Zeitwerk.
|
||||
|
||||
If your application uses Single Table Inheritance, please see the [Single Table Inheritance section](autoloading_and_reloading_constants.html#single-table-inheritance) of the Autoloading and Reloading Constants (Zeitwerk Mode) guide.
|
||||
|
||||
|
||||
### Qualified Names in Class and Module Definitions Are Now Possible
|
||||
|
||||
You can now robustly use constant paths in class and module definitions:
|
||||
|
||||
```ruby
|
||||
# Autoloading in this class' body matches Ruby semantics now.
|
||||
# Autoloading in this class body matches Ruby semantics now.
|
||||
class Admin::UsersController < ApplicationController
|
||||
# ...
|
||||
end
|
||||
@ -383,7 +414,7 @@ end
|
||||
|
||||
### Thread-safety Everywhere
|
||||
|
||||
In classic mode, constant autoloading is not thread-safe, though Rails has locks in place for example to make web requests thread-safe.
|
||||
In `classic` mode, constant autoloading is not thread-safe, though Rails has locks in place for example to make web requests thread-safe.
|
||||
|
||||
Constant autoloading is thread-safe in `zeitwerk` mode. For example, you can now autoload in multi-threaded scripts executed by the `runner` command.
|
||||
|
||||
|
@ -1405,6 +1405,13 @@ If set to `false`:
|
||||
|
||||
The default value is `true` for new apps. Upgraded apps will have that value set to `false` for backwards-compatibility.
|
||||
|
||||
#### `config.active_support.executor_around_test_case`
|
||||
|
||||
Configure the test suite to call `Rails.application.executor.wrap` around test cases.
|
||||
This makes test cases behave closer to an actual request or job.
|
||||
Several features that are normally disabled in test, such as Active Record query cache
|
||||
and asynchronous queries will then be enabled.
|
||||
|
||||
#### `ActiveSupport::Logger.silencer`
|
||||
|
||||
Is set to `false` to disable the ability to silence logging in a block. The default is `true`.
|
||||
@ -1721,6 +1728,8 @@ Accepts a string for the HTML tag used to wrap attachments. Defaults to `"action
|
||||
- `config.active_support.hash_digest_class`: `OpenSSL::Digest::SHA256`
|
||||
- `config.active_support.cache_format_version`: `7.0`
|
||||
- `config.active_support.remove_deprecated_time_with_zone_name`: `true`
|
||||
- `config.active_support.executor_around_test_case`: `true`
|
||||
- `config.active_support.use_rfc4122_namespaced_uuids`: `true`
|
||||
- `config.action_dispatch.return_only_request_media_type_on_content_type`: `false`
|
||||
- `config.action_controller.silence_disabled_session_errors`: `false`
|
||||
- `config.action_mailer.smtp_timeout`: `5`
|
||||
@ -1807,6 +1816,8 @@ Accepts a string for the HTML tag used to wrap attachments. Defaults to `"action
|
||||
- `config.active_support.hash_digest_class`: `OpenSSL::Digest::MD5`
|
||||
- `config.active_support.key_generator_hash_digest_class`: `OpenSSL::Digest::SHA1`
|
||||
- `config.active_support.cache_format_version`: `6.1`
|
||||
- `config.active_support.executor_around_test_case`: `false`
|
||||
- ``config.active_support.use_rfc4122_namespaced_uuids``: `false`
|
||||
- `config.action_dispatch.return_only_request_media_type_on_content_type`: `true`
|
||||
- `ActiveSupport.utc_to_local_returns_utc_offset_times`: `false`
|
||||
- `config.action_mailer.smtp_timeout`: `nil`
|
||||
|
@ -1766,7 +1766,7 @@ and in `app/controllers/comments_controller.rb`:
|
||||
end
|
||||
```
|
||||
|
||||
Within the `article` model, after running a migration to add a `status` column, you would add:
|
||||
Within the `article` model, after running a migration to add a `status` column using `bin/rails db:migrate` command, you would add:
|
||||
|
||||
```ruby
|
||||
class Article < ApplicationRecord
|
||||
|
@ -1972,6 +1972,54 @@ class ChatRelayJobTest < ActiveJob::TestCase
|
||||
end
|
||||
```
|
||||
|
||||
Testing Eager Loading
|
||||
---------------------
|
||||
|
||||
Normally, applications do not eager load in the `development` or `test` environments to speed things up. But they do in the `production` environment.
|
||||
|
||||
If some file in the project cannot be loaded for whatever reason, you better detect it before deploying to production, right?
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
If your project has CI in place, eager loading in CI is an easy way to ensure the application eager loads.
|
||||
|
||||
CIs typically set some environment variable to indicate the test suite is running there. For example, it could be `CI`:
|
||||
|
||||
```ruby
|
||||
# config/environments/test.rb
|
||||
config.eager_load = ENV["CI"].present?
|
||||
```
|
||||
|
||||
Starting with Rails 7, newly generated applications are configured that way by default.
|
||||
|
||||
### Bare Test Suites
|
||||
|
||||
If your project does not have continuous integration, you can still eager load in the test suite by calling `Rails.application.eager_load!`:
|
||||
|
||||
#### minitest
|
||||
|
||||
```ruby
|
||||
require "test_helper"
|
||||
|
||||
class ZeitwerkComplianceTest < ActiveSupport::TestCase
|
||||
test "eager loads all files without errors" do
|
||||
assert_nothing_raised { Rails.application.eager_load! }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### RSpec
|
||||
|
||||
```ruby
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Zeitwerk compliance" do
|
||||
it "eager loads all files without errors" do
|
||||
expect { Rails.application.eager_load! }.not_to raise_error
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Additional Testing Resources
|
||||
----------------------------
|
||||
|
||||
|
@ -44,7 +44,7 @@ def paths
|
||||
exclude: ["assets", javascript_path]
|
||||
paths.add "app/assets", glob: "*"
|
||||
paths.add "app/controllers", eager_load: true
|
||||
paths.add "app/channels", eager_load: true, glob: "**/*_channel.rb"
|
||||
paths.add "app/channels", eager_load: true
|
||||
paths.add "app/helpers", eager_load: true
|
||||
paths.add "app/models", eager_load: true
|
||||
paths.add "app/mailers", eager_load: true
|
||||
|
@ -313,7 +313,7 @@ def jbuilder_gemfile_entry
|
||||
def javascript_gemfile_entry
|
||||
return [] if options[:skip_javascript]
|
||||
|
||||
if options[:javascript] == "importmap"
|
||||
if adjusted_javascript_option == "importmap"
|
||||
GemfileEntry.version("importmap-rails", ">= 0.3.4", "Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]")
|
||||
else
|
||||
GemfileEntry.version "jsbundling-rails", "~> 0.1.0", "Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails]"
|
||||
@ -336,6 +336,16 @@ def using_node?
|
||||
options[:javascript] && options[:javascript] != "importmap"
|
||||
end
|
||||
|
||||
# CSS processors other than Tailwind require a node-based JavaScript environment. So overwrite the normal JS default
|
||||
# if one such processor has been specified.
|
||||
def adjusted_javascript_option
|
||||
if options[:css] && options[:css] != "tailwind" && options[:javascript] == "importmap"
|
||||
"esbuild"
|
||||
else
|
||||
options[:javascript]
|
||||
end
|
||||
end
|
||||
|
||||
def css_gemfile_entry
|
||||
return [] unless options[:css]
|
||||
|
||||
@ -407,9 +417,9 @@ def run_bundle
|
||||
def run_javascript
|
||||
return if options[:skip_javascript] || !bundle_install?
|
||||
|
||||
case options[:javascript]
|
||||
case adjusted_javascript_option
|
||||
when "importmap" then rails_command "importmap:install"
|
||||
when "webpack", "esbuild", "rollup" then rails_command "javascript:install:#{options[:javascript]}"
|
||||
when "webpack", "esbuild", "rollup" then rails_command "javascript:install:#{adjusted_javascript_option}"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<%%= form_with(model: <%= model_resource_name %>) do |form| %>
|
||||
<%% if <%= singular_table_name %>.errors.any? %>
|
||||
<div id="error_explanation">
|
||||
<div style="color: red">
|
||||
<h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
|
||||
|
||||
<ul>
|
||||
@ -12,26 +12,26 @@
|
||||
<%% end %>
|
||||
|
||||
<% attributes.each do |attribute| -%>
|
||||
<div class="field">
|
||||
<div>
|
||||
<% if attribute.password_digest? -%>
|
||||
<%%= form.label :password %>
|
||||
<%%= form.label :password, style: "display: block" %>
|
||||
<%%= form.password_field :password %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%%= form.label :password_confirmation %>
|
||||
<div>
|
||||
<%%= form.label :password_confirmation, style: "display: block" %>
|
||||
<%%= form.password_field :password_confirmation %>
|
||||
<% elsif attribute.attachments? -%>
|
||||
<%%= form.label :<%= attribute.column_name %> %>
|
||||
<%%= form.label :<%= attribute.column_name %>, style: "display: block" %>
|
||||
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, multiple: true %>
|
||||
<% else -%>
|
||||
<%%= form.label :<%= attribute.column_name %> %>
|
||||
<%%= form.label :<%= attribute.column_name %>, style: "display: block" %>
|
||||
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %>
|
||||
<% end -%>
|
||||
</div>
|
||||
|
||||
<% end -%>
|
||||
<div class="actions">
|
||||
<div>
|
||||
<%%= form.submit %>
|
||||
</div>
|
||||
<%% end %>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<p id="notice"><%%= notice %></p>
|
||||
<p style="color: green"><%%= notice %></p>
|
||||
|
||||
<h1><%= human_name.pluralize %></h1>
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div id="<%%= dom_id <%= singular_table_name %> %>" class="scaffold_record">
|
||||
<div id="<%%= dom_id <%= singular_table_name %> %>">
|
||||
<% attributes.reject(&:password_digest?).each do |attribute| -%>
|
||||
<p>
|
||||
<strong><%= attribute.human_name %>:</strong>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<p id="notice"><%%= notice %></p>
|
||||
<p style="color: green"><%%= notice %></p>
|
||||
|
||||
<%%= render @<%= singular_table_name %> %>
|
||||
|
||||
|
@ -46,8 +46,8 @@
|
||||
# Rails.application.config.active_support.cache_format_version = 7.0
|
||||
|
||||
# Calls `Rails.application.executor.wrap` around test cases.
|
||||
# This makes test cases behave closer to an actual request or job, several
|
||||
# features that are normally disabled in test, such as Active Record query cache
|
||||
# This makes test cases behave closer to an actual request or job.
|
||||
# Several features that are normally disabled in test, such as Active Record query cache
|
||||
# and asynchronous queries will then be enabled.
|
||||
# Rails.application.config.active_support.executor_around_test_case = true
|
||||
|
||||
@ -97,3 +97,10 @@
|
||||
# Previously this was set in an initializer. It's fine to keep using that initializer if you've customized it.
|
||||
# To disable parameter wrapping entirely, set this config to `false`.
|
||||
# Rails.application.config.action_controller.wrap_parameters_by_default = true
|
||||
|
||||
# Specifies whether generated namespaced UUIDs follow the RFC 4122 standard for namespace IDs provided as a
|
||||
# `String` to `Digest::UUID.uuid_v3` or `Digest::UUID.uuid_v5` method calls.
|
||||
#
|
||||
# See https://guides.rubyonrails.org/configuring.html#config-active-support-use-rfc4122-namespaced-uuids for
|
||||
# more information
|
||||
# Rails.application.config.active_support.use_rfc4122_namespaced_uuids = true
|
||||
|
@ -124,7 +124,7 @@
|
||||
</header>
|
||||
|
||||
<% if @part && @part.mime_type %>
|
||||
<iframe seamless name="messageBody" src="?<%= part_query(@part.mime_type) %>"></iframe>
|
||||
<iframe name="messageBody" src="?<%= part_query(@part.mime_type) %>"></iframe>
|
||||
<% else %>
|
||||
<p>
|
||||
You are trying to preview an email that does not have any content.
|
||||
|
@ -2422,18 +2422,18 @@ def index
|
||||
assert_equal 1234, ActiveSupport.test_parallelization_threshold
|
||||
end
|
||||
|
||||
test "ActiveSupport.use_rfc4122_namespaced_uuids is enabled by default for new apps" do
|
||||
test "Digest::UUID.use_rfc4122_namespaced_uuids is enabled by default for new apps" do
|
||||
app "development"
|
||||
|
||||
assert_equal true, ActiveSupport.use_rfc4122_namespaced_uuids
|
||||
assert_equal true, Digest::UUID.use_rfc4122_namespaced_uuids
|
||||
end
|
||||
|
||||
test "ActiveSupport.use_rfc4122_namespaced_uuids is disabled by default for upgraded apps" do
|
||||
test "Digest::UUID.use_rfc4122_namespaced_uuids is disabled by default for upgraded apps" do
|
||||
remove_from_config '.*config\.load_defaults.*\n'
|
||||
|
||||
app "development"
|
||||
|
||||
assert_equal false, ActiveSupport.use_rfc4122_namespaced_uuids
|
||||
assert_equal false, Digest::UUID.use_rfc4122_namespaced_uuids
|
||||
end
|
||||
|
||||
test "custom serializers should be able to set via config.active_job.custom_serializers in an initializer" do
|
||||
|
@ -585,7 +585,7 @@ def foo
|
||||
|
||||
get "/rails/mailers/notifier/foo.txt"
|
||||
assert_equal 200, last_response.status
|
||||
assert_match '<iframe seamless name="messageBody" src="?part=text%2Fplain">', last_response.body
|
||||
assert_match '<iframe name="messageBody" src="?part=text%2Fplain">', last_response.body
|
||||
assert_match '<option selected value="part=text%2Fplain">', last_response.body
|
||||
assert_match '<option value="part=text%2Fhtml">', last_response.body
|
||||
|
||||
@ -595,7 +595,7 @@ def foo
|
||||
|
||||
get "/rails/mailers/notifier/foo.html?name=Ruby"
|
||||
assert_equal 200, last_response.status
|
||||
assert_match '<iframe seamless name="messageBody" src="?name=Ruby&part=text%2Fhtml">', last_response.body
|
||||
assert_match '<iframe name="messageBody" src="?name=Ruby&part=text%2Fhtml">', last_response.body
|
||||
assert_match '<option selected value="name=Ruby&part=text%2Fhtml">', last_response.body
|
||||
assert_match '<option value="name=Ruby&part=text%2Fplain">', last_response.body
|
||||
|
||||
@ -634,7 +634,7 @@ def foo
|
||||
|
||||
get "/rails/mailers/notifier/foo"
|
||||
assert_equal 200, last_response.status
|
||||
assert_match %r[<iframe seamless name="messageBody"], last_response.body
|
||||
assert_match %r[<iframe name="messageBody"], last_response.body
|
||||
|
||||
get "/rails/mailers/notifier/foo?part=text/plain"
|
||||
assert_equal 200, last_response.status
|
||||
@ -675,7 +675,7 @@ def foo
|
||||
|
||||
get "/rails/mailers/notifier/foo"
|
||||
assert_equal 200, last_response.status
|
||||
assert_match %r[<iframe seamless name="messageBody"], last_response.body
|
||||
assert_match %r[<iframe name="messageBody"], last_response.body
|
||||
|
||||
get "/rails/mailers/notifier/foo?part=text/plain"
|
||||
assert_equal 200, last_response.status
|
||||
@ -721,7 +721,7 @@ def foo
|
||||
|
||||
get "/rails/mailers/notifier/foo"
|
||||
assert_equal 200, last_response.status
|
||||
assert_match %r[<iframe seamless name="messageBody"], last_response.body
|
||||
assert_match %r[<iframe name="messageBody"], last_response.body
|
||||
|
||||
get "/rails/mailers/notifier/foo?part=text/plain"
|
||||
assert_equal 200, last_response.status
|
||||
@ -779,7 +779,7 @@ def foo
|
||||
|
||||
get "/rails/mailers/notifier/foo"
|
||||
assert_equal 200, last_response.status
|
||||
assert_match %r[<iframe seamless name="messageBody"], last_response.body
|
||||
assert_match %r[<iframe name="messageBody"], last_response.body
|
||||
|
||||
get "/rails/mailers/notifier/foo?part=text/plain"
|
||||
assert_equal 200, last_response.status
|
||||
|
Loading…
Reference in New Issue
Block a user