Merge branch 'master' into ac_params_exists
This commit is contained in:
commit
74bef0970f
@ -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
|
||||
|
62
actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb
Normal file
62
actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb
Normal file
@ -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
Loading…
Reference in New Issue
Block a user