Merge branch 'master' into ac_params_exists

This commit is contained in:
Aaron Patterson 2019-01-11 11:21:53 -08:00 committed by GitHub
commit 74bef0970f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 705 additions and 396 deletions

@ -191,6 +191,12 @@ Lint/StringConversionInInterpolation:
Lint/UriEscapeUnescape:
Enabled: true
Lint/UselessAssignment:
Enabled: true
Lint/DeprecatedClassMethods:
Enabled: true
Style/ParenthesesAroundCondition:
Enabled: true

@ -109,13 +109,14 @@ browser.
This will stop you from looking silly when you push an RC to rubygems.org and
then realize it is broken.
### Release to RubyGems and NPM.
### Release to RubyGems and npm.
IMPORTANT: The Action Cable client and Action View's UJS adapter are released
as NPM packages, so you must have Node.js installed, have an NPM account
(npmjs.com), and be a package owner for `actioncable` and `rails-ujs` (you can
check this via `npm owner ls actioncable` and `npm owner ls rails-ujs`) in
order to do a full release. Do not release until you're set up with NPM!
IMPORTANT: Several gems have JavaScript components that are released as npm
packages, so you must have Node.js installed, have an npm account (npmjs.com),
and be a package owner for `@rails/actioncable`, `@rails/actiontext`,
`@rails/activestorage`, and `@rails/ujs`. You can check this by making sure your
npm user (`npm whoami`) is listed as an owner (`npm owner ls <pkg>`) of each
package. Do not release until you're set up with npm!
The release task will sign the release tag. If you haven't got commit signing
set up, use https://git-scm.com/book/tr/v2/Git-Tools-Signing-Your-Work as a

@ -504,7 +504,7 @@ WebSocket functionality.
### Installation
```
npm install actioncable --save
npm install @rails/actioncable --save
```
### Usage
@ -516,7 +516,7 @@ provided.
In JavaScript...
```javascript
ActionCable = require('actioncable')
ActionCable = require('@rails/actioncable')
var cable = ActionCable.createConsumer('wss://RAILS-API-PATH.com/cable')
@ -528,7 +528,7 @@ cable.subscriptions.create('AppearanceChannel', {
and in CoffeeScript...
```coffeescript
ActionCable = require('actioncable')
ActionCable = require('@rails/actioncable')
cable = ActionCable.createConsumer('wss://RAILS-API-PATH.com/cable')

@ -1,6 +1,6 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
import ActionCable from "actioncable"
import ActionCable from "@rails/actioncable"
export default ActionCable.createConsumer()

@ -1,5 +1,5 @@
{
"name": "actioncable",
"name": "@rails/actioncable",
"version": "6.0.0-alpha",
"description": "WebSocket framework for Ruby on Rails.",
"main": "app/assets/javascripts/action_cable.js",

@ -1,6 +1,6 @@
# Action Mailbox
Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, and SendGrid. You can also handle inbound mails directly via the built-in Postfix ingress.
Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Postfix ingress.
The inbound emails are turned into `InboundEmail` records using Active Record and feature lifecycle tracking, storage of the original email on cloud storage via Active Storage, and responsible data handling with on-by-default incineration.

@ -4,6 +4,8 @@ require "bundler/setup"
require "bundler/gem_tasks"
require "rake/testtask"
task :package
Rake::TestTask.new do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"

@ -37,7 +37,7 @@ class Ingresses::Amazon::InboundEmailsController < BaseController
def self.prepare
self.verifier ||= begin
require "aws-sdk-sns/message_verifier"
require "aws-sdk-sns"
Aws::SNS::MessageVerifier.new
end
end

@ -0,0 +1,62 @@
# frozen_string_literal: true
module ActionMailbox
# Ingests inbound emails from Postmark. Requires a +RawEmail+ parameter containing a full RFC 822 message.
#
# Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
# password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
#
# Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
# the Postmark ingress can learn its password. You should only use the Postmark ingress over HTTPS.
#
# Returns:
#
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postmark
# - <tt>422 Unprocessable Entity</tt> if the request is missing the required +RawEmail+ parameter
# - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
# the Active Storage service, or the Active Job backend is misconfigured or unavailable
#
# == Usage
#
# 1. Tell Action Mailbox to accept emails from Postmark:
#
# # config/environments/production.rb
# config.action_mailbox.ingress = :postmark
#
# 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postmark ingress.
#
# Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
#
# action_mailbox:
# ingress_password: ...
#
# Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
#
# 3. {Configure Postmark}[https://postmarkapp.com/manual#configure-your-inbound-webhook-url] to forward inbound emails
# to +/rails/action_mailbox/postmark/inbound_emails+ with the username +actionmailbox+ and the password you
# previously generated. If your application lived at <tt>https://example.com</tt>, you would configure your
# Postmark inbound webhook with the following fully-qualified URL:
#
# https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails
#
# *NOTE:* When configuring your Postmark inbound webhook, be sure to check the box labeled *"Include raw email
# content in JSON payload"*. Action Mailbox needs the raw email content to work.
class Ingresses::Postmark::InboundEmailsController < ActionMailbox::BaseController
before_action :authenticate_by_password
def create
ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("RawEmail")
rescue ActionController::ParameterMissing => error
logger.error <<~MESSAGE
#{error.message}
When configuring your Postmark inbound webhook, be sure to check the box
labeled "Include raw email content in JSON payload".
MESSAGE
head :unprocessable_entity
end
end
end

@ -5,6 +5,7 @@
post "/amazon/inbound_emails" => "amazon/inbound_emails#create", as: :rails_amazon_inbound_emails
post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails
post "/postfix/inbound_emails" => "postfix/inbound_emails#create", as: :rails_postfix_inbound_emails
post "/postmark/inbound_emails" => "postmark/inbound_emails#create", as: :rails_postmark_inbound_emails
post "/sendgrid/inbound_emails" => "sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails
# Mailgun requires that a webhook's URL end in 'mime' for it to receive the raw contents of emails.

@ -0,0 +1,55 @@
# frozen_string_literal: true
require "test_helper"
class ActionMailbox::Ingresses::Postmark::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
setup { ActionMailbox.ingress = :postmark }
test "receiving an inbound email from Postmark" do
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
post rails_postmark_inbound_emails_url,
headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
end
assert_response :no_content
inbound_email = ActionMailbox::InboundEmail.last
assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
end
test "rejecting when RawEmail param is missing" do
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
post rails_postmark_inbound_emails_url,
headers: { authorization: credentials }, params: { From: "someone@example.com" }
end
assert_response :unprocessable_entity
end
test "rejecting an unauthorized inbound email from Postmark" do
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
post rails_postmark_inbound_emails_url, params: { RawEmail: file_fixture("../files/welcome.eml").read }
end
assert_response :unauthorized
end
test "raising when the configured password is nil" do
switch_password_to nil do
assert_raises ArgumentError do
post rails_postmark_inbound_emails_url,
headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
end
end
end
test "raising when the configured password is blank" do
switch_password_to "" do
assert_raises ArgumentError do
post rails_postmark_inbound_emails_url,
headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
end
end
end
end

@ -461,7 +461,7 @@ def _protected_ivars # :nodoc:
helper ActionMailer::MailHelper
class_attribute :delivery_job, default: ::ActionMailer::MailDeliveryJob
class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob
class_attribute :default_params, default: {
mime_version: "1.0",
charset: "UTF-8",

@ -145,12 +145,20 @@ def enqueue_delivery(delivery_method, options = {})
if processed?
super
else
job = @mailer_class.delivery_job
job = delivery_job_class
args = arguments_for(job, delivery_method)
job.set(options).perform_later(*args)
end
end
def delivery_job_class
if @mailer_class.delivery_job <= MailDeliveryJob
@mailer_class.delivery_job
else
Parameterized::DeliveryJob
end
end
def arguments_for(delivery_job, delivery_method)
if delivery_job <= MailDeliveryJob
[@mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args]

@ -46,6 +46,10 @@ class Railtie < Rails::Railtie # :nodoc:
register_preview_interceptors(options.delete(:preview_interceptors))
register_observers(options.delete(:observers))
if delivery_job = options.delete(:delivery_job)
self.delivery_job = delivery_job.constantize
end
options.each { |k, v| send("#{k}=", v) }
end

@ -33,6 +33,8 @@ def self.root
FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__)
ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
ActionMailer::Base.delivery_job = ActionMailer::MailDeliveryJob
class ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions

@ -11,9 +11,6 @@ class LegacyDeliveryJobTest < ActiveSupport::TestCase
class LegacyDeliveryJob < ActionMailer::DeliveryJob
end
class LegacyParmeterizedDeliveryJob < ActionMailer::Parameterized::DeliveryJob
end
setup do
@previous_logger = ActiveJob::Base.logger
ActiveJob::Base.logger = Logger.new(nil)
@ -42,9 +39,9 @@ class LegacyParmeterizedDeliveryJob < ActionMailer::Parameterized::DeliveryJob
{ inviter: "david@basecamp.com", invitee: "jason@basecamp.com" },
]
with_delivery_job(LegacyParmeterizedDeliveryJob) do
with_delivery_job(LegacyDeliveryJob) do
assert_deprecated do
assert_performed_with(job: LegacyParmeterizedDeliveryJob, args: args) do
assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: args) do
mail.deliver_later
end
end

@ -241,22 +241,8 @@ def inherited(klass)
# Performs parameters wrapping upon the request. Called automatically
# by the metal call stack.
def process_action(*args)
if _wrapper_enabled?
wrapped_hash = _wrap_parameters request.request_parameters
wrapped_keys = request.request_parameters.keys
wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
# This will make the wrapped hash accessible from controller and view.
request.parameters.merge! wrapped_hash
request.request_parameters.merge! wrapped_hash
# This will display the wrapped hash in the log file.
request.filtered_parameters.merge! wrapped_filtered_hash
end
ensure
# NOTE: Rescues all exceptions so they
# may be caught in ActionController::Rescue.
return super
_perform_parameter_wrapping if _wrapper_enabled?
super
end
private
@ -292,5 +278,20 @@ def _wrapper_enabled?
ref = request.content_mime_type.ref
_wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
end
def _perform_parameter_wrapping
wrapped_hash = _wrap_parameters request.request_parameters
wrapped_keys = request.request_parameters.keys
wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
# This will make the wrapped hash accessible from controller and view.
request.parameters.merge! wrapped_hash
request.request_parameters.merge! wrapped_hash
# This will display the wrapped hash in the log file.
request.filtered_parameters.merge! wrapped_filtered_hash
rescue ActionDispatch::Http::Parameters::ParseError
# swallow parse error exception
end
end
end

@ -197,10 +197,12 @@ def merge_and_normalize_cache_control!(cache_control)
if control.empty?
# Let middleware handle default behavior
elsif control[:no_cache]
self._cache_control = NO_CACHE
if control[:extras]
self._cache_control = _cache_control + ", #{control[:extras].join(', ')}"
end
options = []
options << PUBLIC if control[:public]
options << NO_CACHE
options.concat(control[:extras]) if control[:extras]
self._cache_control = options.join(", ")
else
extras = control[:extras]
max_age = control[:max_age]

@ -888,7 +888,7 @@ def without_exception
yield
# Do stuff...
wtf += 1
wtf + 1
end
end

@ -183,6 +183,11 @@ def conditional_hello_without_expires_and_confliciting_cache_control_headers
render action: "hello_world"
end
def conditional_hello_without_expires_and_public_header
response.headers["Cache-Control"] = "public, no-cache"
render action: "hello_world"
end
def conditional_hello_with_bangs
render action: "hello_world"
end
@ -418,6 +423,11 @@ def test_no_expires_now_with_conflicting_cache_control_headers
assert_equal "no-cache", @response.headers["Cache-Control"]
end
def test_no_expires_now_with_public
get :conditional_hello_without_expires_and_public_header
assert_equal "public, no-cache", @response.headers["Cache-Control"]
end
def test_date_header_when_expires_in
time = Time.mktime(2011, 10, 30)
Time.stub :now, time do

@ -14,7 +14,7 @@ def assign_parameters
end
def dump_params_keys(hash = params)
hash.keys.sort.inject("") do |s, k|
hash.keys.sort.each_with_object(+"") do |k, s|
value = hash[k]
if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
@ -23,8 +23,8 @@ def dump_params_keys(hash = params)
value = ""
end
s += ", " unless s.empty?
s += "#{k}#{value}"
s << ", " unless s.empty?
s << "#{k}#{value}"
end
end
end

@ -4,6 +4,8 @@ require "bundler/setup"
require "bundler/gem_tasks"
require "rake/testtask"
task :package
Rake::TestTask.new do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"

@ -1,4 +1,4 @@
import { DirectUpload } from "activestorage"
import { DirectUpload } from "@rails/activestorage"
export class AttachmentUpload {
constructor(attachment, element) {

@ -25,3 +25,5 @@ def to_plain_text
delegate :blank?, :empty?, :present?, to: :to_plain_text
end
end
ActiveSupport.run_load_hooks :action_text_rich_text, ActionText::RichText

@ -8,15 +8,14 @@
copy_file "#{__dir__}/../../app/views/active_storage/blobs/_blob.html.erb",
"app/views/active_storage/blobs/_blob.html.erb"
# FIXME: Replace with release version on release
say "Installing JavaScript dependency"
run "yarn add https://github.com/rails/actiontext"
run "yarn add @rails/actiontext"
APPLICATION_PACK_PATH = "app/javascript/packs/application.js"
if File.exists?(APPLICATION_PACK_PATH) && File.read(APPLICATION_PACK_PATH) !~ /import "actiontext"/
if File.exist?(APPLICATION_PACK_PATH) && File.read(APPLICATION_PACK_PATH) !~ /import "@rails\/actiontext"/
say "Adding import to default JavaScript pack"
append_to_file APPLICATION_PACK_PATH, <<-EOS
import "actiontext"
import "@rails/actiontext"
EOS
end

@ -1,5 +1,5 @@
{
"name": "actiontext",
"name": "@rails/actiontext",
"version": "6.0.0-alpha",
"description": "Edit and display rich text in Rails applications",
"main": "app/javascript/actiontext/index.js",
@ -21,7 +21,7 @@
],
"license": "MIT",
"dependencies": {
"trix": ">=1.0.0",
"activestorage": "6.0.0-alpha"
"trix": "^1.0.0",
"@rails/activestorage": "^6.0.0-alpha"
}
}

@ -1 +1 @@
import "actiontext"
import "@rails/actiontext"

@ -1,11 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"activestorage@>= 5.2.0-rc1":
version "5.2.0-rc1"
resolved "https://registry.yarnpkg.com/activestorage/-/activestorage-5.2.0-rc1.tgz#79898996eceb0f13575eff41fb109051fbfa49b0"
trix@^0.11.1:
version "0.11.1"
resolved "https://registry.yarnpkg.com/trix/-/trix-0.11.1.tgz#ffe54f2757c2c2385b8424fd5c5d2ab712a09acc"

@ -17,11 +17,11 @@ Note that the `data` attributes this library adds are a feature of HTML5. If you
### NPM
npm install rails-ujs --save
npm install @rails/ujs --save
### Yarn
yarn add rails-ujs
yarn add @rails/ujs
Ensure that `.yarnclean` does not include `assets` if you use [yarn autoclean](https://yarnpkg.com/lang/en/docs/cli/autoclean/).
@ -40,7 +40,7 @@ In a conventional Rails application that uses the asset pipeline, require `rails
If you're using the Webpacker gem or some other JavaScript bundler, add the following to your main JS file:
```javascript
import Rails from 'rails-ujs';
import Rails from "@rails/ujs"
Rails.start()
```

@ -654,7 +654,7 @@ def time_zone_options_for_select(selected = nil, priority_zones = nil, model = :
#
# ==== Gotcha
#
# The HTML specification says when nothing is select on a collection of radio buttons
# The HTML specification says when nothing is selected on a collection of radio buttons
# web browsers do not send any value to server.
# Unfortunately this introduces a gotcha:
# if a +User+ model has a +category_id+ field and in the form no category is selected, no +category_id+ parameter is sent. So,

@ -1,5 +1,5 @@
{
"name": "rails-ujs",
"name": "@rails/ujs",
"version": "6.0.0-alpha",
"description": "Ruby on Rails unobtrusive scripting adapter",
"main": "lib/assets/compiled/rails-ujs.js",

@ -81,7 +81,7 @@ def initialize(*arguments)
@queue_name = self.class.queue_name
@priority = self.class.priority
@executions = 0
@exception_executions = Hash.new(0)
@exception_executions = {}
end
# Returns a hash with the job data that can safely be passed to the

@ -50,8 +50,8 @@ module ClassMethods
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
rescue_from(*exceptions) do |error|
# Guard against jobs that were persisted before we started having individual executions counters per retry_on
self.exception_executions ||= Hash.new(0)
self.exception_executions[exceptions.to_s] += 1
self.exception_executions ||= {}
self.exception_executions[exceptions.to_s] = (exception_executions[exceptions.to_s] || 0) + 1
if exception_executions[exceptions.to_s] < attempts
retry_job wait: determine_delay(wait), queue: queue, priority: priority, error: error

@ -4,30 +4,26 @@
require "jobs/retry_job"
require "models/person"
class ExceptionsTest < ActiveJob::TestCase
class ExceptionsTest < ActiveSupport::TestCase
setup do
JobBuffer.clear
skip if ActiveJob::Base.queue_adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter)
skip if adapter_skips_scheduling?(ActiveJob::Base.queue_adapter)
end
test "successfully retry job throwing exception against defaults" do
perform_enqueued_jobs do
RetryJob.perform_later "DefaultsError", 5
RetryJob.perform_later "DefaultsError", 5
assert_equal [
"Raised DefaultsError for the 1st time",
"Raised DefaultsError for the 2nd time",
"Raised DefaultsError for the 3rd time",
"Raised DefaultsError for the 4th time",
"Successfully completed job" ], JobBuffer.values
end
assert_equal [
"Raised DefaultsError for the 1st time",
"Raised DefaultsError for the 2nd time",
"Raised DefaultsError for the 3rd time",
"Raised DefaultsError for the 4th time",
"Successfully completed job" ], JobBuffer.values
end
test "successfully retry job throwing exception against higher limit" do
perform_enqueued_jobs do
RetryJob.perform_later "ShortWaitTenAttemptsError", 9
assert_equal 9, JobBuffer.values.count
end
RetryJob.perform_later "ShortWaitTenAttemptsError", 9
assert_equal 9, JobBuffer.values.count
end
test "keeps the same attempts counter for several exceptions listed in the same retry_on declaration" do
@ -35,9 +31,7 @@ class ExceptionsTest < ActiveJob::TestCase
SecondRetryableErrorOfTwo SecondRetryableErrorOfTwo)
assert_raises SecondRetryableErrorOfTwo do
perform_enqueued_jobs do
RetryJob.perform_later(exceptions_to_raise, 5)
end
RetryJob.perform_later(exceptions_to_raise, 5)
assert_equal [
"Raised FirstRetryableErrorOfTwo for the 1st time",
@ -54,9 +48,7 @@ class ExceptionsTest < ActiveJob::TestCase
FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo)
assert_nothing_raised do
perform_enqueued_jobs do
RetryJob.perform_later(exceptions_to_raise, 10)
end
RetryJob.perform_later(exceptions_to_raise, 10)
assert_equal [
"Raised DefaultsError for the 1st time",
@ -72,108 +64,107 @@ class ExceptionsTest < ActiveJob::TestCase
end
test "failed retry job when exception kept occurring against defaults" do
perform_enqueued_jobs do
RetryJob.perform_later "DefaultsError", 6
assert_equal "Raised DefaultsError for the 5th time", JobBuffer.last_value
rescue DefaultsError
pass
end
RetryJob.perform_later "DefaultsError", 6
assert_equal "Raised DefaultsError for the 5th time", JobBuffer.last_value
rescue DefaultsError
pass
end
test "failed retry job when exception kept occurring against higher limit" do
perform_enqueued_jobs do
RetryJob.perform_later "ShortWaitTenAttemptsError", 11
assert_equal "Raised ShortWaitTenAttemptsError for the 10th time", JobBuffer.last_value
rescue ShortWaitTenAttemptsError
pass
end
RetryJob.perform_later "ShortWaitTenAttemptsError", 11
assert_equal "Raised ShortWaitTenAttemptsError for the 10th time", JobBuffer.last_value
rescue ShortWaitTenAttemptsError
pass
end
test "discard job" do
perform_enqueued_jobs do
RetryJob.perform_later "DiscardableError", 2
assert_equal "Raised DiscardableError for the 1st time", JobBuffer.last_value
end
RetryJob.perform_later "DiscardableError", 2
assert_equal "Raised DiscardableError for the 1st time", JobBuffer.last_value
end
test "custom handling of discarded job" do
perform_enqueued_jobs do
RetryJob.perform_later "CustomDiscardableError", 2
assert_equal "Dealt with a job that was discarded in a custom way. Message: CustomDiscardableError", JobBuffer.last_value
end
RetryJob.perform_later "CustomDiscardableError", 2
assert_equal "Dealt with a job that was discarded in a custom way. Message: CustomDiscardableError", JobBuffer.last_value
end
test "custom handling of job that exceeds retry attempts" do
perform_enqueued_jobs do
RetryJob.perform_later "CustomCatchError", 6
assert_equal "Dealt with a job that failed to retry in a custom way after 6 attempts. Message: CustomCatchError", JobBuffer.last_value
end
RetryJob.perform_later "CustomCatchError", 6
assert_equal "Dealt with a job that failed to retry in a custom way after 6 attempts. Message: CustomCatchError", JobBuffer.last_value
end
test "long wait job" do
travel_to Time.now
perform_enqueued_jobs do
assert_performed_with at: (Time.now + 3600.seconds).to_i do
RetryJob.perform_later "LongWaitError", 5
end
end
RetryJob.perform_later "LongWaitError", 2, :log_scheduled_at
assert_equal [
"Raised LongWaitError for the 1st time",
"Next execution scheduled at #{(Time.now + 3600.seconds).to_f}",
"Successfully completed job"
], JobBuffer.values
end
test "exponentially retrying job" do
travel_to Time.now
perform_enqueued_jobs do
assert_performed_with at: (Time.now + 3.seconds).to_i do
assert_performed_with at: (Time.now + 18.seconds).to_i do
assert_performed_with at: (Time.now + 83.seconds).to_i do
assert_performed_with at: (Time.now + 258.seconds).to_i do
RetryJob.perform_later "ExponentialWaitTenAttemptsError", 5
end
end
end
end
end
RetryJob.perform_later "ExponentialWaitTenAttemptsError", 5, :log_scheduled_at
assert_equal [
"Raised ExponentialWaitTenAttemptsError for the 1st time",
"Next execution scheduled at #{(Time.now + 3.seconds).to_f}",
"Raised ExponentialWaitTenAttemptsError for the 2nd time",
"Next execution scheduled at #{(Time.now + 18.seconds).to_f}",
"Raised ExponentialWaitTenAttemptsError for the 3rd time",
"Next execution scheduled at #{(Time.now + 83.seconds).to_f}",
"Raised ExponentialWaitTenAttemptsError for the 4th time",
"Next execution scheduled at #{(Time.now + 258.seconds).to_f}",
"Successfully completed job"
], JobBuffer.values
end
test "custom wait retrying job" do
travel_to Time.now
perform_enqueued_jobs do
assert_performed_with at: (Time.now + 2.seconds).to_i do
assert_performed_with at: (Time.now + 4.seconds).to_i do
assert_performed_with at: (Time.now + 6.seconds).to_i do
assert_performed_with at: (Time.now + 8.seconds).to_i do
RetryJob.perform_later "CustomWaitTenAttemptsError", 5
end
end
end
end
end
RetryJob.perform_later "CustomWaitTenAttemptsError", 5, :log_scheduled_at
assert_equal [
"Raised CustomWaitTenAttemptsError for the 1st time",
"Next execution scheduled at #{(Time.now + 2.seconds).to_f}",
"Raised CustomWaitTenAttemptsError for the 2nd time",
"Next execution scheduled at #{(Time.now + 4.seconds).to_f}",
"Raised CustomWaitTenAttemptsError for the 3rd time",
"Next execution scheduled at #{(Time.now + 6.seconds).to_f}",
"Raised CustomWaitTenAttemptsError for the 4th time",
"Next execution scheduled at #{(Time.now + 8.seconds).to_f}",
"Successfully completed job"
], JobBuffer.values
end
test "successfully retry job throwing one of two retryable exceptions" do
perform_enqueued_jobs do
RetryJob.perform_later "SecondRetryableErrorOfTwo", 3
RetryJob.perform_later "SecondRetryableErrorOfTwo", 3
assert_equal [
"Raised SecondRetryableErrorOfTwo for the 1st time",
"Raised SecondRetryableErrorOfTwo for the 2nd time",
"Successfully completed job" ], JobBuffer.values
end
assert_equal [
"Raised SecondRetryableErrorOfTwo for the 1st time",
"Raised SecondRetryableErrorOfTwo for the 2nd time",
"Successfully completed job" ], JobBuffer.values
end
test "discard job throwing one of two discardable exceptions" do
perform_enqueued_jobs do
RetryJob.perform_later "SecondDiscardableErrorOfTwo", 2
assert_equal [ "Raised SecondDiscardableErrorOfTwo for the 1st time" ], JobBuffer.values
end
RetryJob.perform_later "SecondDiscardableErrorOfTwo", 2
assert_equal [ "Raised SecondDiscardableErrorOfTwo for the 1st time" ], JobBuffer.values
end
test "successfully retry job throwing DeserializationError" do
perform_enqueued_jobs do
RetryJob.perform_later Person.new(404), 5
assert_equal ["Raised ActiveJob::DeserializationError for the 5 time"], JobBuffer.values
end
RetryJob.perform_later Person.new(404), 5
assert_equal ["Raised ActiveJob::DeserializationError for the 5 time"], JobBuffer.values
end
private
def adapter_skips_scheduling?(queue_adapter)
[
ActiveJob::QueueAdapters::InlineAdapter,
ActiveJob::QueueAdapters::AsyncAdapter,
ActiveJob::QueueAdapters::SneakersAdapter
].include?(queue_adapter.class)
end
end

@ -30,7 +30,13 @@ class RetryJob < ActiveJob::Base
discard_on FirstDiscardableErrorOfTwo, SecondDiscardableErrorOfTwo
discard_on(CustomDiscardableError) { |job, error| JobBuffer.add("Dealt with a job that was discarded in a custom way. Message: #{error.message}") }
def perform(raising, attempts)
before_enqueue do |job|
if job.arguments.include?(:log_scheduled_at) && job.scheduled_at
JobBuffer.add("Next execution scheduled at #{job.scheduled_at}")
end
end
def perform(raising, attempts, *)
raising = raising.shift if raising.is_a?(Array)
if raising && executions < attempts
JobBuffer.add("Raised #{raising} for the #{executions.ordinalize} time")

@ -1,7 +1,12 @@
* Allow `ActionController::Params` as argument of `ActiveRecord::Base#exists?`.
*Gannon McGibbon*
* Add support for endless ranges introduces in Ruby 2.6.
*Greg Navis*
* Deprecate passing `migrations_paths` to `connection.assume_migrated_upto_version`.
*Ryuta Kamizono*

@ -123,7 +123,7 @@ def exec_query(sql, name = "SQL", binds = [], prepare: false)
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
sql, binds = sql_for_insert(sql, pk, nil, sequence_name, binds)
sql, binds = sql_for_insert(sql, pk, sequence_name, binds)
exec_query(sql, name, binds)
end
@ -464,7 +464,7 @@ def select_prepared(sql, name = nil, binds = [])
exec_query(sql, name, binds, prepare: true)
end
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
def sql_for_insert(sql, pk, sequence_name, binds)
[sql, binds]
end

@ -504,15 +504,17 @@ def raw_connection
@connection
end
def case_sensitive_comparison(table, attribute, column, value) # :nodoc:
table[attribute].eq(value)
def case_sensitive_comparison(attribute, value) # :nodoc:
attribute.eq(value)
end
def case_insensitive_comparison(table, attribute, column, value) # :nodoc:
def case_insensitive_comparison(attribute, value) # :nodoc:
column = column_for_attribute(attribute)
if can_perform_case_insensitive_comparison_for?(column)
table[attribute].lower.eq(table.lower(value))
attribute.lower.eq(attribute.relation.lower(value))
else
table[attribute].eq(value)
attribute.eq(value)
end
end
@ -659,6 +661,11 @@ def column_for(table_name, column_name)
raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}")
end
def column_for_attribute(attribute)
table_name = attribute.relation.name
schema_cache.columns_hash(table_name)[attribute.name.to_s]
end
def collector
if prepared_statements
Arel::Collectors::Composite.new(

@ -476,9 +476,11 @@ def primary_keys(table_name) # :nodoc:
SQL
end
def case_sensitive_comparison(table, attribute, column, value) # :nodoc:
def case_sensitive_comparison(attribute, value) # :nodoc:
column = column_for_attribute(attribute)
if column.collation && !column.case_sensitive?
table[attribute].eq(Arel::Nodes::Bin.new(value))
attribute.eq(Arel::Nodes::Bin.new(value))
else
super
end
@ -579,13 +581,13 @@ def initialize_type_map(m = type_map)
m.alias_type %r(bit)i, "binary"
m.register_type(%r(enum)i) do |sql_type|
limit = sql_type[/^enum\((.+)\)/i, 1]
limit = sql_type[/^enum\s*\((.+)\)/i, 1]
.split(",").map { |enum| enum.strip.length - 2 }.max
MysqlString.new(limit: limit)
end
m.register_type(%r(^set)i) do |sql_type|
limit = sql_type[/^set\((.+)\)/i, 1]
limit = sql_type[/^set\s*\((.+)\)/i, 1]
.split(",").map { |set| set.strip.length - 1 }.sum - 1
MysqlString.new(limit: limit)
end

@ -110,7 +110,7 @@ def exec_delete(sql, name = nil, binds = [])
end
alias :exec_update :exec_delete
def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc:
def sql_for_insert(sql, pk, sequence_name, binds) # :nodoc:
if pk.nil?
# Extract the table from the insert sql. Yuck.
table_ref = extract_table_ref_from_insert_sql(sql)

@ -22,8 +22,8 @@ def recreate_database(name, options = {}) #:nodoc:
def create_database(name, options = {})
options = { encoding: "utf8" }.merge!(options.symbolize_keys)
option_string = options.inject("") do |memo, (key, value)|
memo += case key
option_string = options.each_with_object(+"") do |(key, value), memo|
memo << case key
when :owner
" OWNER = \"#{value}\""
when :template

@ -44,6 +44,11 @@ def arel_attribute(name) # :nodoc:
end
def bind_attribute(name, value) # :nodoc:
if reflection = klass._reflect_on_association(name)
name = reflection.foreign_key
value = value.read_attribute(reflection.klass.primary_key) unless value.nil?
end
attr = arel_attribute(name)
bind = predicate_builder.build_bind_attribute(attr.name, value)
yield attr, bind

@ -3,11 +3,7 @@
module ActiveRecord
class PredicateBuilder
class RangeHandler # :nodoc:
class RangeWithBinds < Struct.new(:begin, :end)
def exclude_end?
false
end
end
RangeWithBinds = Struct.new(:begin, :end, :exclude_end?)
def initialize(predicate_builder)
@predicate_builder = predicate_builder
@ -16,22 +12,7 @@ def initialize(predicate_builder)
def call(attribute, value)
begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin)
end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end)
if begin_bind.value.infinity?
if end_bind.value.infinity?
attribute.not_in([])
elsif value.exclude_end?
attribute.lt(end_bind)
else
attribute.lteq(end_bind)
end
elsif end_bind.value.infinity?
attribute.gteq(begin_bind)
elsif value.exclude_end?
attribute.gteq(begin_bind).and(attribute.lt(end_bind))
else
attribute.between(RangeWithBinds.new(begin_bind, end_bind))
end
attribute.between(RangeWithBinds.new(begin_bind, end_bind, value.exclude_end?))
end
private

@ -30,12 +30,12 @@ def boundable?
@_boundable = false
end
def infinity?
_infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database)
def infinite?
infinity?(value_before_type_cast) || boundable? && infinity?(value_for_database)
end
private
def _infinity?(value)
def infinity?(value)
value.respond_to?(:infinite?) && value.infinite?
end
end

@ -56,7 +56,7 @@ module ClassMethods # :nodoc:
def touch_attributes_with_time(*names, time: nil)
attribute_names = timestamp_attributes_for_update_in_model
attribute_names |= names.map(&:to_s)
attribute_names.index_with(time ||= current_time_from_proper_timezone)
attribute_names.index_with(time || current_time_from_proper_timezone)
end
private

@ -56,33 +56,21 @@ def find_finder_class_for(record)
end
def build_relation(klass, attribute, value)
if reflection = klass._reflect_on_association(attribute)
attribute = reflection.foreign_key
value = value.attributes[reflection.klass.primary_key] unless value.nil?
relation = klass.unscoped
comparison = relation.bind_attribute(attribute, value) do |attr, bind|
return relation.none! unless bind.boundable?
if bind.nil?
attr.eq(bind)
elsif options[:case_sensitive]
klass.connection.case_sensitive_comparison(attr, bind)
else
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(attr, bind)
end
end
if value.nil?
return klass.unscoped.where!(attribute => value)
end
# the attribute may be an aliased attribute
if klass.attribute_alias?(attribute)
attribute = klass.attribute_alias(attribute)
end
attribute_name = attribute.to_s
value = klass.predicate_builder.build_bind_attribute(attribute_name, value)
table = klass.arel_table
column = klass.columns_hash[attribute_name]
comparison = if !options[:case_sensitive]
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
klass.connection.case_sensitive_comparison(table, attribute, column, value)
end
klass.unscoped.where!(comparison)
relation.where!(comparison)
end
def scope_relation(record, relation)

@ -24,6 +24,10 @@ def nil?
value.nil?
end
def infinite?
value.respond_to?(:infinite?) && value.infinite?
end
def boundable?
!value.respond_to?(:boundable?) || value.boundable?
end

@ -27,6 +27,10 @@ def eql?(other)
class Quoted < Arel::Nodes::Unary # :nodoc:
alias :val :value
def nil?; val.nil?; end
def infinite?
value.respond_to?(:infinite?) && value.infinite?
end
end
def self.build_quoted(other, attribute = nil)

@ -35,15 +35,15 @@ def eq_all(others)
end
def between(other)
if equals_quoted?(other.begin, -Float::INFINITY)
if equals_quoted?(other.end, Float::INFINITY)
if infinity?(other.begin)
if other.end.nil? || infinity?(other.end)
not_in([])
elsif other.exclude_end?
lt(other.end)
else
lteq(other.end)
end
elsif equals_quoted?(other.end, Float::INFINITY)
elsif other.end.nil? || infinity?(other.end)
gteq(other.begin)
elsif other.exclude_end?
gteq(other.begin).and(lt(other.end))
@ -81,15 +81,15 @@ def in_all(others)
end
def not_between(other)
if equals_quoted?(other.begin, -Float::INFINITY)
if equals_quoted?(other.end, Float::INFINITY)
if infinity?(other.begin)
if infinity?(other.end)
self.in([])
elsif other.exclude_end?
gteq(other.end)
else
gt(other.end)
end
elsif equals_quoted?(other.end, Float::INFINITY)
elsif infinity?(other.end)
lt(other.begin)
else
left = lt(other.begin)
@ -238,12 +238,8 @@ def quoted_array(others)
others.map { |v| quoted_node(v) }
end
def equals_quoted?(maybe_quoted, value)
if maybe_quoted.is_a?(Nodes::Quoted)
maybe_quoted.val == value
else
maybe_quoted == value
end
def infinity?(value)
value.respond_to?(:infinite?) && value.infinite?
end
end
end

@ -15,8 +15,9 @@ def visit_Arel_Nodes_SelectStatement(o, collector)
collector << "ORDER BY "
collector = inject_join o.orders, collector, ", "
end
collector = maybe_visit o.lock, collector
maybe_visit o.lock, collector
end
def visit_Arel_Nodes_SelectCore(o, collector)
collector = inject_join o.projections, collector, ", "
if o.source && !o.source.empty?

@ -20,7 +20,7 @@ def visit_Arel_Nodes_SelectStatement(o, collector)
def visit_Arel_Nodes_SelectOptions(o, collector)
collector = maybe_visit o.offset, collector
collector = maybe_visit o.limit, collector
collector = maybe_visit o.lock, collector
maybe_visit o.lock, collector
end
def visit_Arel_Nodes_Limit(o, collector)

@ -208,14 +208,12 @@ def visit_Arel_Nodes_SelectStatement(o, collector)
end
visit_Arel_Nodes_SelectOptions(o, collector)
collector
end
def visit_Arel_Nodes_SelectOptions(o, collector)
collector = maybe_visit o.limit, collector
collector = maybe_visit o.offset, collector
collector = maybe_visit o.lock, collector
maybe_visit o.lock, collector
end
def visit_Arel_Nodes_SelectCore(o, collector)

@ -7,22 +7,21 @@ class Default < ActiveRecord::Base; end
def test_case_insensitiveness
connection = ActiveRecord::Base.connection
table = Default.arel_table
column = Default.columns_hash["char1"]
comparison = connection.case_insensitive_comparison table, :char1, column, nil
attr = Default.arel_attribute(:char1)
comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
column = Default.columns_hash["char2"]
comparison = connection.case_insensitive_comparison table, :char2, column, nil
attr = Default.arel_attribute(:char2)
comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
column = Default.columns_hash["char3"]
comparison = connection.case_insensitive_comparison table, :char3, column, nil
attr = Default.arel_attribute(:char3)
comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
column = Default.columns_hash["multiline_default"]
comparison = connection.case_insensitive_comparison table, :multiline_default, column, nil
attr = Default.arel_attribute(:multiline_default)
comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
end
end

@ -639,6 +639,18 @@ class AttributeTest < Arel::Spec
)
end
if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION)
it "can be constructed with a range implicitly ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser
node.must_equal Nodes::GreaterThanOrEqual.new(
attribute,
Nodes::Casted.new(0, attribute)
)
end
end
it "can be constructed with a quoted range ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(quoted_range(0, ::Float::INFINITY, false))

@ -430,7 +430,7 @@ def not_a_post.id
assert_kind_of ActiveRecord::Relation, relation
assert_kind_of Post, relation.first
relation = [not_a_post] * relation.count
[not_a_post] * relation.count
end
end
end

@ -27,8 +27,12 @@ def test_boolean_types
def test_string_types
assert_lookup_type :string, "enum('one', 'two', 'three')"
assert_lookup_type :string, "ENUM('one', 'two', 'three')"
assert_lookup_type :string, "enum ('one', 'two', 'three')"
assert_lookup_type :string, "ENUM ('one', 'two', 'three')"
assert_lookup_type :string, "set('one', 'two', 'three')"
assert_lookup_type :string, "SET('one', 'two', 'three')"
assert_lookup_type :string, "set ('one', 'two', 'three')"
assert_lookup_type :string, "SET ('one', 'two', 'three')"
end
def test_set_type_with_value_matching_other_type

@ -144,7 +144,7 @@ class ::SpecialReply < ::Reply
test "update other counters on parent destroy" do
david, joanna = dog_lovers(:david, :joanna)
joanna = joanna # squelch a warning
_ = joanna # squelch a warning
assert_difference "joanna.reload.dogs_count", -1 do
david.destroy

@ -240,7 +240,7 @@ def test_alt_becomes_bang_resets_inheritance_type_column
cabbage = vegetable.becomes!(Cabbage)
assert_equal "Cabbage", cabbage.custom_type
vegetable = cabbage.becomes!(Vegetable)
cabbage.becomes!(Vegetable)
assert_nil cabbage.custom_type
end
@ -654,7 +654,7 @@ def test_polymorphic_associations_custom_type
assert_equal ["omg_inheritance_attribute_mapping_test/company"], ActiveRecord::Base.connection.select_values("SELECT sponsorable_type FROM sponsors")
sponsor = Sponsor.first
sponsor = Sponsor.find(sponsor.id)
assert_equal startup, sponsor.sponsorable
end
end

@ -22,7 +22,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialize_does_not_eagerly_load_columns
Topic.reset_column_information
reset_column_information_of(Topic)
assert_no_queries do
Topic.serialize(:content)
end
@ -377,7 +377,8 @@ def test_serialized_attribute_works_under_concurrent_initial_access
topic.update group: "1"
model.serialize :group, JSON
model.reset_column_information
reset_column_information_of(model)
# This isn't strictly necessary for the test, but a little bit of
# knowledge of internals allows us to make failures far more likely.
@ -397,4 +398,12 @@ def test_serialized_attribute_works_under_concurrent_initial_access
# raw string ("1"), or raise an exception.
assert_equal [1] * threads.size, threads.map(&:value)
end
private
def reset_column_information_of(topic_class)
topic_class.reset_column_information
# reset original topic to undefine attribute methods
::Topic.reset_column_information
end
end

@ -118,7 +118,7 @@ Active Storage, with its included JavaScript library, supports uploading directl
```
Using the npm package:
```js
import * as ActiveStorage from "activestorage"
import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
```
2. Annotate file inputs with the direct upload URL.

@ -1,5 +1,5 @@
{
"name": "activestorage",
"name": "@rails/activestorage",
"version": "6.0.0-alpha",
"description": "Attach cloud and local files in Rails applications",
"main": "app/assets/javascripts/activestorage.js",

@ -17,6 +17,7 @@
# Enable/disable caching. By default caching is disabled.
if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {

@ -1,3 +1,13 @@
* Fix `String#safe_constantize` throwing a `LoadError` for incorrectly cased constant references.
*Keenan Brock*
* Preserve key order passed to `ActiveSupport::CacheStore#fetch_multi`.
`fetch_multi(*names)` now returns its results in the same order as the `*names` requested, rather than returning cache hits followed by cache misses.
*Gannon McGibbon*
* If the same block is `included` multiple times for a Concern, an exception is no longer raised.
*Mark J. Titorenko*, *Vlad Bokov*

@ -438,18 +438,18 @@ def fetch_multi(*names)
options = merged_options(options)
instrument :read_multi, names, options do |payload|
read_multi_entries(names, options).tap do |results|
payload[:hits] = results.keys
payload[:super_operation] = :fetch_multi
writes = {}
(names - results.keys).each do |name|
results[name] = writes[name] = yield(name)
end
write_multi writes, options
reads = read_multi_entries(names, options)
writes = {}
ordered = names.each_with_object({}) do |name, hash|
hash[name] = reads.fetch(name) { writes[name] = yield(name) }
end
payload[:hits] = reads.keys
payload[:super_operation] = :fetch_multi
write_multi(writes, options)
ordered
end
end

@ -214,8 +214,11 @@ def initialize(value, parts) #:nodoc:
end
def coerce(other) #:nodoc:
if Scalar === other
case other
when Scalar
[other, self]
when Duration
[Scalar.new(other.value), self]
else
[Scalar.new(other), self]
end

@ -328,6 +328,8 @@ def safe_constantize(camel_cased_word)
e.name.to_s == camel_cased_word.to_s)
rescue ArgumentError => e
raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match?(e.message)
rescue LoadError => e
raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(e.message)
end
# Returns the suffix that should be added to a number to denote the position

@ -0,0 +1,4 @@
# frozen_string_literal: true
# raises a load error typical of the dynamic code that manually raises load errors
raise LoadError, "required gem not present kind of error"

@ -130,7 +130,7 @@ def test_fetch_multi_without_expires_in
assert_equal("fufu", @cache.read("fu"))
end
def test_multi_with_objects
def test_fetch_multi_with_objects
cache_struct = Struct.new(:cache_key, :title)
foo = cache_struct.new("foo", "FOO!")
bar = cache_struct.new("bar")
@ -142,6 +142,14 @@ def test_multi_with_objects
assert_equal({ foo => "FOO!", bar => "BAM!" }, values)
end
def test_fetch_multi_returns_ordered_names
@cache.write("bam", "BAM")
values = @cache.fetch_multi("foo", "bar", "bam") { |key| key.upcase }
assert_equal(%w(foo bar bam), values.keys)
end
def test_fetch_multi_without_block
assert_raises(ArgumentError) do
@cache.fetch_multi("foo")

@ -112,6 +112,16 @@ def run_safe_constantize_tests_on
assert_nil yield("A::Object::B")
assert_nil yield("A::Object::Object::Object::B")
with_autoloading_fixtures do
assert_nil yield("Em")
end
assert_raises(LoadError) do
with_autoloading_fixtures do
yield("RaisesLoadError")
end
end
assert_raises(NameError) do
with_autoloading_fixtures do
yield("RaisesNameError")

@ -104,7 +104,7 @@ def test_negative_differences
def test_expression_is_evaluated_in_the_appropriate_scope
silence_warnings do
local_scope = "foo"
local_scope = local_scope # to suppress unused variable warning
_ = local_scope # to suppress unused variable warning
assert_difference("local_scope; @object.num") { @object.increment }
end
end

@ -63,9 +63,9 @@ def generate_mobi
end
def mobi
mobi = "ruby_on_rails_guides_#{@version || @edge[0, 7]}"
mobi += ".#{@language}" if @language
mobi += ".mobi"
mobi = +"ruby_on_rails_guides_#{@version || @edge[0, 7]}"
mobi << ".#{@language}" if @language
mobi << ".mobi"
end
def initialize_dirs

@ -3,6 +3,7 @@
require "redcarpet"
require "nokogiri"
require "rails_guides/markdown/renderer"
require "rails-html-sanitizer"
module RailsGuides
class Markdown
@ -20,6 +21,7 @@ def render(body)
@raw_body = body
extract_raw_header_and_body
generate_header
generate_description
generate_title
generate_body
generate_structure
@ -82,6 +84,11 @@ def generate_header
@header = engine.render(@raw_header).html_safe
end
def generate_description
sanitizer = Rails::Html::FullSanitizer.new
@description = sanitizer.sanitize(@header).squish
end
def generate_structure
@headings_for_index = []
if @body.present?
@ -165,6 +172,7 @@ def node_index(hierarchy)
def render_page
@view.content_for(:header_section) { @header }
@view.content_for(:description) { @description }
@view.content_for(:page_title) { @title }
@view.content_for(:index_section) { @index }
@view.render(layout: @layout, html: @body.html_safe)

@ -151,7 +151,7 @@ established using the following JavaScript, which is generated by default by Rai
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
import ActionCable from "actioncable"
import ActionCable from "@rails/actioncable"
export default ActionCable.createConsumer()
```

@ -20,8 +20,8 @@ Introduction
Action Mailbox routes incoming emails to controller-like mailboxes for
processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill,
and SendGrid. You can also handle inbound mails directly via the built-in
Postfix ingress.
Postmark, and SendGrid. You can also handle inbound mails directly via the
built-in Postfix ingress.
The inbound emails are turned into `InboundEmail` records using Active Record
and feature lifecycle tracking, storage of the original email on cloud storage
@ -155,6 +155,42 @@ would look like this:
$ URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... rails action_mailbox:ingress:postfix
```
### Postmark
Tell Action Mailbox to accept emails from Postmark:
```ruby
# config/environments/production.rb
config.action_mailbox.ingress = :postmark
```
Generate a strong password that Action Mailbox can use to authenticate
requests to the Postmark ingress.
Use `rails credentials:edit` to add the password to your application's
encrypted credentials under `action_mailbox.ingress_password`,
where Action Mailbox will automatically find it:
```yaml
action_mailbox:
ingress_password: ...
```
Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD`
environment variable.
[Configure Postmark inbound webhook](https://postmarkapp.com/manual#configure-your-inbound-webhook-url)
to forward inbound emails to `/rails/action_mailbox/postmark/inbound_emails` with the username `actionmailbox`
and the password you previously generated. If your application lived at `https://example.com`, you would
configure Postmark with the following fully-qualified URL:
```
https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails
```
NOTE: When configuring your Postmark inbound webhook, be sure to check the box labeled **"Include raw email content in JSON payload"**.
Action Mailbox needs the raw email content to work.
### SendGrid
Tell Action Mailbox to accept emails from SendGrid:

@ -105,9 +105,9 @@ depending on the purpose of these columns.
fields that Active Record will look for when you create associations between
your models.
* **Primary keys** - By default, Active Record will use an integer column named
`id` as the table's primary key. When using [Active Record
Migrations](active_record_migrations.html) to create your tables, this column will be
automatically created.
`id` as the table's primary key (`bigint` for Postgres and MYSQL, `integer`
for SQLite). When using [Active Record Migrations](active_record_migrations.html)
to create your tables, this column will be automatically created.
There are also some optional column names that will add additional features
to Active Record instances:

@ -489,7 +489,7 @@ directly from the client to the cloud.
Using the npm package:
```js
import * as ActiveStorage from "activestorage"
import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
```
@ -616,7 +616,7 @@ of choice, instantiate a DirectUpload and call its create method. Create takes
a callback to invoke when the upload completes.
```js
import { DirectUpload } from "activestorage"
import { DirectUpload } from "@rails/activestorage"
const input = document.querySelector('input[type=file]')
@ -664,7 +664,7 @@ will call the object's `directUploadWillStoreFileWithXHR` method. You can then
bind your own progress handler on the XHR.
```js
import { DirectUpload } from "activestorage"
import { DirectUpload } from "@rails/activestorage"
class Uploader {
constructor(file, url) {

@ -721,6 +721,8 @@ There are a number of settings available on `config.action_mailer`:
* `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. By default this is `false` in all environments.
* `config.action_mailer.delivery_job` specifies delivery job for mail. Defaults to `ActionMailer::DeliveryJob`.
### Configuring Active Support
@ -905,6 +907,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
- `config.action_view.default_enforce_utf8`: `false`
- `config.action_dispatch.use_cookies_with_metadata`: `true`
- `config.action_mailer.delivery_job`: `"ActionMailer::MailDeliveryJob"`
- `config.active_job.return_false_on_aborted_enqueue`: `true`
- `config.active_storage.queues.analysis`: `:active_storage_analysis`
- `config.active_storage.queues.purge`: `:active_storage_purge`

@ -1511,6 +1511,7 @@ To hook into the initialization process of one of the following classes use the
| `ActionMailer::Base` | `action_mailer` |
| `ActionMailer::TestCase` | `action_mailer_test_case` |
| `ActionText::Content` | `action_text_content` |
| `ActionText::RichText` | `action_text_rich_text` |
| `ActionView::Base` | `action_view` |
| `ActionView::TestCase` | `action_view_test_case` |
| `ActiveJob::Base` | `active_job` |

@ -139,10 +139,12 @@ Note that appending directly to `I18n.load_paths` instead of to the application'
### Managing the Locale across Requests
The default locale is used for all translations unless `I18n.locale` is explicitly set.
A localized application will likely need to provide support for multiple locales. To accomplish this, the locale should be set at the beginning of each request so that all strings are translated using the desired locale during the lifetime of that request.
The default locale is used for all translations unless `I18n.locale=` or `I18n.with_locale` is used.
`I18n.locale` can leak into subsequent requests served by the same thread/process if it is not consistently set in every controller. For example executing `I18n.locale = :es` in one POST requests will have effects for all later requests to controllers that don't set the locale, but only in that particular thread/process. For that reason, instead of `I18n.locale =` you can use `I18n.with_locale` which does not have this leak issue.
The locale can be set in an `around_action` in the `ApplicationController`:
```ruby

@ -1,6 +1,5 @@
<% content_for :page_title do %>
Ruby on Rails Guides
<% end %>
<% content_for :page_title, "Ruby on Rails Guides" %>
<% content_for :description, "Ruby on Rails Guides" %>
<% content_for :header_section do %>
<%= render 'welcome' %>

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= yield(:page_title) || 'Ruby on Rails Guides' %></title>
<title><%= yield(:page_title) %></title>
<link rel="stylesheet" type="text/css" href="stylesheets/style.css" data-turbolinks-track="reload">
<link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print">
<link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shCore.css" data-turbolinks-track="reload">
@ -14,6 +14,13 @@
<script src="javascripts/turbolinks.js" data-turbolinks-track="reload"></script>
<script src="javascripts/guides.js" data-turbolinks-track="reload"></script>
<script src="javascripts/responsive-tables.js" data-turbolinks-track="reload"></script>
<meta property="og:title" content="<%= yield(:page_title) %>" />
<meta name="description" content="<%= yield(:description) %>" />
<meta property="og:description" content="<%= yield(:description) %>" />
<meta property="og:locale" content="en_US" />
<meta property="og:site_name" content="Ruby on Rails Guides" />
<meta property="og:image" content="https://avatars.githubusercontent.com/u/4223" />
<meta property="og:type" content="website" />
</head>
<body class="guide">
<% if @edge %>

@ -519,7 +519,7 @@ resources :photos do
end
```
You can leave out the `:on` option, this will create the same member route except that the resource id value will be available in `params[:photo_id]` instead of `params[:id]`.
You can leave out the `:on` option, this will create the same member route except that the resource id value will be available in `params[:photo_id]` instead of `params[:id]`. Route helpers will also be renamed from `preview_photo_url` and `preview_photo_path` to `photo_preview_url` and `photo_preview_path`.
#### Adding Collection Routes

@ -2,6 +2,7 @@
"private": true,
"workspaces": [
"actioncable",
"actiontext",
"activestorage",
"actionview",
"tmp/templates/app_template",

@ -1,3 +1,7 @@
* Use original `bundler` environment variables during the process of generating a new rails project.
*Marco Costa*
* Send Active Storage analysis and purge jobs to dedicated queues by default.
Analysis jobs now use the `:active_storage_analysis` queue, and purge jobs
@ -71,12 +75,6 @@
*Gannon McGibbon*
* Add JSON support to rails properties route (`/rails/info/properties`).
Now, `Rails::Info` properties may be accessed in JSON format at `/rails/info/properties.json`.
*Yoshiyuki Hirano*
* Use Ids instead of memory addresses when displaying references in scaffold views.
Fixes #29200.

@ -130,6 +130,10 @@ def load_defaults(target_version)
action_dispatch.use_cookies_with_metadata = true
end
if respond_to?(:action_mailer)
action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
end
if respond_to?(:active_job)
active_job.return_false_on_aborted_enqueue = true
end

@ -95,8 +95,8 @@ def print_splitter
end
def print_line(name, statistics)
m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0
loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0
m_over_c = (statistics.methods / statistics.classes) rescue 0
loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue 0
print "| #{name.ljust(20)} "
HEADERS.each_key do |k|

@ -388,19 +388,21 @@ def bundle_command(command, env = {})
# its own vendored Thor, which could be a different version. Running both
# things in the same process is a recipe for a night with paracetamol.
#
# We unset temporary bundler variables to load proper bundler and Gemfile.
#
# Thanks to James Tucker for the Gem tricks involved in this call.
_bundle_command = Gem.bin_path("bundler", "bundle")
require "bundler"
Bundler.with_clean_env do
full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" #{command}]
if options[:quiet]
system(env, full_command, out: File::NULL)
else
system(env, full_command)
end
Bundler.with_original_env do
exec_bundle_command(_bundle_command, command, env)
end
end
def exec_bundle_command(bundle_command, command, env)
full_command = %Q["#{Gem.ruby}" "#{bundle_command}" #{command}]
if options[:quiet]
system(env, full_command, out: File::NULL)
else
system(env, full_command)
end
end

@ -1,6 +1,6 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
import ActionCable from "actioncable"
import ActionCable from "@rails/actioncable"
export default ActionCable.createConsumer()

@ -3,7 +3,7 @@
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
import Rails from "rails-ujs"
import Rails from "@rails/ujs"
Rails.start()
<%- unless options[:skip_turbolinks] -%>
@ -12,7 +12,7 @@ Turbolinks.start()
<%- end -%>
<%- unless skip_active_storage? -%>
import * as ActiveStorage from "activestorage"
import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
<%- end -%>
<%- unless options[:skip_action_cable] -%>

@ -24,7 +24,7 @@ test:
<<: *default
database: <%= app_name %>_test
# As with config/secrets.yml, you never want to store sensitive information,
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#

@ -60,7 +60,7 @@ test:
<<: *default
database: <%= app_name[0,4] %>_tst
# As with config/secrets.yml, you never want to store sensitive information,
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#

@ -54,7 +54,7 @@ test:
<<: *default
url: jdbc:db://localhost/<%= app_name %>_test
# As with config/secrets.yml, you never want to store sensitive information,
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#

@ -27,7 +27,7 @@ test:
<<: *default
database: <%= app_name %>_test
# As with config/secrets.yml, you never want to store sensitive information,
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#

@ -43,7 +43,7 @@ test:
<<: *default
database: <%= app_name %>_test
# As with config/secrets.yml, you never want to store sensitive information,
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#

@ -32,7 +32,7 @@ test:
<<: *default
database: <%= app_name %>_test
# As with config/secrets.yml, you never want to store sensitive information,
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#

@ -33,7 +33,7 @@ test:
<<: *default
database: <%= app_name %>_test
# As with config/secrets.yml, you never want to store sensitive information,
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#

@ -59,7 +59,7 @@ test:
<<: *default
database: <%= app_name %>_test
# As with config/secrets.yml, you never want to store sensitive information,
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#

@ -26,7 +26,7 @@ test:
<<: *default
database: <%= app_name %>_test
# As with config/secrets.yml, you never want to store sensitive information,
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#

@ -16,6 +16,7 @@ Rails.application.configure do
# Run rails dev:cache to toggle caching.
if Rails.root.join('tmp', 'caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {

@ -22,3 +22,12 @@
# Send Active Storage analysis and purge jobs to dedicated queues.
# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis
# Rails.application.config.active_storage.queues.purge = :active_storage_purge
# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail.
#
# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions.
# If you send mail in the background, job workers need to have a copy of
# MailDeliveryJob to ensure all delivery jobs are processed properly.
# Make sure your entire app is migrated and stable on 6.0 before using this setting.
# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"

Some files were not shown because too many files have changed in this diff Show More