如何为在数据库中存储为日期时间的复选框属性添加表单标签?

How to add form tag for a checbox attribute that is stored as a datetime in the database?

假设我有一个名为 advertisements 的 table。我需要为其添加一个字段,以便人们可以将其标记为 activeinactive。我可以添加一个名为 inactive 的 boolean 字段,这是一种直接的方法。

还有另一种方法,我可以创建一个数据类型为 datetime 的字段 inactive。这不仅告诉我广告是否 active/no,还告诉我广告何时停用。因此,如果该值为 null,则它处于活动状态,如果它是时间戳,则它在过去的那个时间被停用。

但是,问题在于将它添加到表单助手中。 对于复选框实现,Rails 建议如下:

= f.check_box :inactive, { class: 'something' }

这与数据库中的 boolean 数据类型配合得很好。 我如何使用 datetime 值使它工作?

谢谢!

尝试

= f.check_box :inactive, checked: form.object.inactive.present?,  { class: 'something' }

Active Record 为模型中的每个属性提供 query methods,它们以属性名称以问号结尾(例如,对于 model.name 属性有 model.name?)命名当属性为空时为 false,否则为 true。此外,在处理数值时,如果值为零,查询方法将 return false。

AR 查询方法将为您的 inactive? 标志提供 getter,您需要添加 setter 以允许保存对 inactive? 属性的更改。

跟踪停用时间

添加一列deactivated_atdatetime

add_column :advertisements, :deactivated_at, :datetime

形式

<%= form.text_field :deactivated_at, class: "add-datetimepicker" %>

在字段上添加日期时间选择器

bootstrap-datetimepicker
datetimepicker

假设我们有一个允许参数的方法

def advertisment_params
  params.require(:advertisement).permit(:inactive)
end

通过将方法更改为

def advertisment_params
  params[:advertisement][:inactive] = params[:advertisement][:inactive] == "0" ? nil : @advertisement.nil? ? DateTime.now.to_s(:db) : @advertisement.inactive
  params.require(:advertisement).permit(:inactive)
end

会成功的。

回答我自己的问题。因此,使用 check_box 帮助程序,您可以指定何时应将复选框显示为 checked,您还可以指定当有人选中该框时要分配的值。

我做到了

= f.check_box :inactive, { class: 'something', checked: f.object.inactive.present? }, f.object.inactive.nil? ? DateTime.now : f.object.inactive

最后一部分 DateTime.now 是用户选中复选框时分配的值。这样您就可以使用复选框实现来保存 DateTime 值。值的存在可帮助您确定在显示表单时是否应选中该复选框。

此方法的唯一注意事项是值的准确性。用户可能会在打开表单几次 mins/hours 后提交表单。如果捕获正确的值对您很重要,您可以使用 JS 来分配准确的值或在控制器中包含逻辑。

以下是我对实现在控制器级别完成的布尔参数到日期时间转换的支持的看法。这解决了 中的问题,其中由于日期时间是在编辑视图中而不是在 PUT/PATCH.

时生成的,时间戳已过时

代码被编写为与 Rails 6. YMMV 在其他版本上一起工作。

首先,您需要 table 上的时间戳列。我们称模型为 Resource 和 table 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、操作和其他参数的存在,我排除了详细信息。

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 参数。

如果满足其中 none 个条件,则翻译将继续。

三进制用于确定分配给 inactive_at 参数的翻译值是时间戳还是 nil。转换用于将 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 内置了 classes,可以对各种对象进行类型转换。 ActiveRecord::Type::Boolean就是我们感兴趣的

编辑:

作为额外的奖励,这是在 RSpec 中进行的测试功能的请求测试。控制器测试可能更合适‍♂️

我就不去line-by-line解释了,留待reader自己去解读测试吧

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