这是我对在控制器级别实现对布尔参数到日期时间转换的支持的看法。这解决了 accepted answer 中的问题,由于日期时间是在编辑视图中而不是在 PUT/PATCH 时生成的,因此时间戳过时。
编写代码以与 Rails 6 一起使用。其他版本上的 YMMV。
首先,您需要表格中的时间戳列。我们称模型为Resource,表为resources。在迁移中添加日期时间字段inactive_at。
def change
add_column :resources, :inactive_at, :datetime
end
我假设在控制器中,资源被实例化并分配给@resource。
在视图表单中,您需要在表单助手块中包含以下复选框。
f.check_box :inactive, checked: @resource.inactive_at?
checked: @resource.inactive_at? 设置复选框的默认值。我们将使用参数inactive 来承载是否更新inactive_at 的布尔信息。
在您的控制器中,您需要以下内容。为简洁起见,我排除了控制器是否继承父类、操作以及是否存在其他参数的详细信息。
class ResourceController
private
def resource_params
translate_inactive_param
params.require(:resource).permit(:inactive_at)
end
def translate_inactive_param
return if params[:resource][:inactive_at].present?
return if changing_inactive_at_is_not_requested?
return if touching_inactive_at_is_requested_and_inactive_at_is_already_present?
return if removing_inactive_at_is_requested_and_inactive_at_is_already_blank?
params[:resource][:inactive_at] = cast_to_boolean(inactive_param) ? Time.current : nil
end
def changing_inactive_at_is_not_requested?
inactive_param.nil?
end
def touching_inactive_at_is_requested_and_inactive_is_already_present?
cast_to_boolean(inactive_param) && @resource.inactive_at?
end
def removing_inactive_at_is_requested_and_inactive_at_is_already_blank?
!cast_to_boolean(inactive_param) && @resource.inactive_at.blank?
end
def inactive_param
params.dig(:resource, :inactive)
end
def cast_to_boolean(value)
ActiveRecord::Type::Boolean.new.cast(value)
end
end
我会详细介绍每种方法。
def resource_params
transform_inactive_param
params.require(:resource).permit(:inactive_at)
end
这是strong parameters的方法。
在返回强参数之前,具有布尔值的inactive 参数将被转换为inactive_at 的日期时间对象。
对于inactive 的布尔值,Rail 的表单助手将为false 输出"0",为true 输出"1"。
对于typecasting in Rails,以下内容被认为是虚假的:false, 0, "0", :"0", "f", :f, "F", :F, "false", :false, "FALSE", :FALSE, "off", :off, "OFF", :OFF
对于truthy,它是nil以外的任何值。
对于此示例,我们将假设参数中未定义的inactive 或分配的nil 值表示不请求更改inactive_at。如果请求从请求负载中省略inactive,这是一个合理的假设。
def translate_inactive_param
return if params[:resource][:inactive_at].present?
return if changing_published_at_is_not_requested?
return if touching_inactive_at_is_requested_and_inactive_at_is_already_present?
return if removing_inactive_at_is_requested_and_inactive_at_is_already_blank?
params[:resource][:inactive_at] = cast_to_boolean(inactive_param) ? Time.current : nil
end
此方法将inactive 参数转换为inactive_at 参数。
这里使用了保护子句模式。我们希望翻译跳过一些条件。
-
params[:resource][:inactive_at].present? — 我们优先考虑已经存在的 inactive_at 参数而不是 inactive。如果在控制器中多次调用 resource_params,则很有用。
-
changing_published_at_is_not_requested? — 如果@resource.inactive_at 已经存在并且inactive 参数为真,那么我们不想触及该字段。我假设不需要更新时间戳。
-
removing_inactive_at_is_requested_and_inactive_at_is_already_blank? — 如果@resource.inactive_at 已经为空且inactive 参数为假,则无需转换为inactive_at 参数。
如果这些条件都不满足,则继续翻译。
三元组用于确定分配给inactive_at 参数的转换值是时间戳还是nil。 Casting 用于将inactive_param 转换为布尔值。
def changing_inactive_at_is_not_requested?
inactive_param.nil?
end
通过方法名称自我解释。拉入一个方法,通过给它一个描述性的方法名称来明确它的作用。
def touching_inactive_at_is_requested_and_inactive_is_already_present?
cast_to_boolean(inactive_param) && @resource.inactive_at?
end
通过方法名称自我解释。拉入一个方法,通过给它一个描述性的方法名称来明确它的作用。
def removing_inactive_at_is_requested_and_inactive_at_is_already_blank?
!cast_to_boolean(inactive_param) && @resource.inactive_at.blank?
end
通过方法名称自我解释。拉入一个方法,通过给它一个描述性的方法名称来明确它的作用。
def inactive_param
params.dig(:resource, :inactive)
end
从params 中访问inactive 参数的值。由于这被使用了几次,我将它提取到一个方法中。
def cast_to_boolean(value)
ActiveRecord::Type::Boolean.new.cast(value)
end
转换“布尔”值,特别是来自 Rails 表单助手或通过 HTTP 完成的任何请求。 Rails 内置了对各种对象进行类型转换的类。 ActiveRecord::Type::Boolean 是我们感兴趣的那个。
编辑:
作为额外的奖励,这里有一个在 RSpec 中进行的请求测试,用于测试功能。控制器测试可能更合适??♂️
我不会逐行解释它,所以我将把它留给读者来解释测试。
RSpec::Matchers.define_negated_matcher(:not_change, :change)
RSpec.describe 'PATCH/PUT /resources/:id/edit', type: :request do
include ActiveSupport::Testing::TimeHelpers
shared_examples 'update `inactive_at` using parameter `inactive`' do
describe '`inactive` parameter' do
let(:resource) { Resource.create(inactive_at: nil) }
let(:params) { { resource: { inactive: nil } } }
subject { -> { resource.reload } }
it { is_expected.to(not_change { resource.inactive_at }) }
context 'when `inactive` key is form value falsey "0"' do
let(:params) { { resource: { inactive: '0' } } }
it { is_expected.to(not_change { resource.inactive_at }) }
end
context 'when `inactive` key is form value truthy "1"' do
let(:params) { { resource: { inactive: '1' } } }
around { |example| freeze_time { example.run } }
it { is_expected.to(change { resource.inactive_at }.to(Time.current)) }
end
context 'when `inactive_at` is present in the resource' do
let(:resource) { Resource.create(inactive_at: 1.year.ago) }
it { is_expected.to(not_change { resource.inactive_at }) }
context 'and when `inactive` key is form value falsey "0"' do
let(:params) { { resource: { inactive: '0' } } }
it { is_expected.to(change { resource.inactive_at }.to(nil)) }
end
context 'and when `inactive` key is form value truthy "1"' do
let(:params) { { resource: { inactive: '1' } } }
it { is_expected.to(not_change { resource.inactive_at }) }
end
end
end
end
describe 'PATCH' do
before do
patch resource_path(resource), params: params
end
include_examples 'update `inactive_at` using parameter `inactive`'
end
describe 'PUT' do
before do
put resource_path(resource), params: params
end
include_examples 'update `inactive_at` using parameter `inactive`'
end
end