Rails嵌套属性和自定义验证

时间:2010-09-02 00:29:19

标签: ruby-on-rails validation associations nested-forms

我对Ruby和Rails都很陌生(使用2.3.8),所以请原谅我,如果我错过了一些非常明显的东西,但我一直在努力解决这个问题,而且我的搜索毫无结果。

在我的代码中,我有计划,计划有很多Plan_Steps。每个Plan_Step都有一个数字(表示'1st','2nd'等)。我有一个更新计划的表单,我需要验证每个Plan_Step都有一个唯一的编号。下面的代码可能会更好地解释设计:

模型/ plan.rb:

Class Plan < ActiveRecord::Base
  has_many :plan_steps
  accepts_nested_attributes_for :plan_steps, :allow_destroy => true

  validate :validate_unique_step_numbers

  # Require all steps to be a unique number
  def validate_unique_step_numbers
    step_numbers = []
    plan_steps.each do |step|
      #puts step.description
      if !step.marked_for_destruction? && step_numbers.include?(step.number) 
        errors.add("Error Here")
      elsif !step.marked_for_destruction?
        step_numbers << step.number
      end
  end      
end

控制器/ plans_controller.rb:

...
def update
  @plan = Plan.find(params[:id])
  if @plan.update_attributes(params[:plan])
    #Success
  else
    #Fail
  end
end

现在,当我的表单提交更新时,params哈希看起来像这样:

  {"commit"=>"Submit", 
   "action"=>"update", 
   "_method"=>"put",
   "authenticity_token"=>"NHUfDqRDFSFSFSFspaCuvi/WAAOFpg5AAANMre4x/uu8=", 
   "id"=>"1", 
   "plan"=>{
     "name"=>"Plan Name", 
     "plan_steps_attributes"=>{
       "0"=>{"number"=>"1", "id"=>"1", "_destroy"=>"0", "description"=>"one"}, 
       "1"=>{"number"=>"2", "id"=>"3", "_destroy"=>"0", "description"=>"three"}, 
       "2"=>{"id"=>"2", "_destroy"=>"1"}},            
   "controller"=>"plans"}

数据库包含Plan_Steps的条目,其中包含以下内容:

ID=1, Number=1, Description='one'
ID=2, Number=2, Description='two'

请注意,ID = 2存在且Number = 2,我要做的是删除ID = 2并创建一个Number = 2的新条目(ID = 3)。

好的,所以设置完毕,这是我的问题:

当我在验证中调用plan_steps时,它似乎是从数据库中提取值而不是从传递给update_attributes的params []数组中提取值。

例如,如果我在验证中取消注释'puts'行,我会看到数据库中存在的Plan_Steps描述,而不是传入参数中存在的描述。这意味着我无法验证传入的Plan_Steps。

我也无法在Plan_Steps模型中进行验证,因为除非我错了,否则将对数据库进行验证(而不是传入的参数)。

如果这是一个措辞不好的问题,我道歉,但这是相当具体的。如果您需要任何澄清,请询问。

请记住,我是一个菜鸟,所以我很容易犯一些非常愚蠢的错误。

2 个答案:

答案 0 :(得分:1)

据我所知,您在模型中执行的任何验证都将查看数据库。如果要比较参数中的值,则需要在达到db验证之前执行此操作(完全不推荐)。此外,仅供将来参考,您可以使用内置的validates_uniqueness_of来实现验证:

validates_uniqueness_of :number, :scope => :plan_id

至于你最终想要完成什么(并且记住我对你的项目知之甚少,所以请你带着一点点盐),我建议计算一下步后端而不是依赖于用户输入。我会提出具体建议,但如果不知道如何收集“数字”值(拖放,手动输入,列表位置等等),很难说。

答案 1 :(得分:0)

433887,

我为你的问题写了一些测试,因为我不确定accept_nested_attributes是如何在内部工作的。如果在传递的参数中包含'id'属性,那么不存在的记录会被忽略,如果记录不存在,请参见下文。

#test/fixtures/plans.yml
only_plan:
   id: 1

#test/fixtures/plan_steps.yml
one:
  plan_id: 1
  number: 1
  description: one

two:
  plan_id: 1
  number: 2
  description: two

#test/unit/plan_test.rb
require 'test_helper'

class PlanTest < ActiveSupport::TestCase

  # These are just helpers I like to use so that Test::Unit gives good 
  # feedback as to which call you're testing.
  def assert_to(assump, inst_sub, meth, *args )
    assert_equal assump, instance_variable_get(inst_sub).send(meth, *args), 
    "#{inst_sub}.#{meth}(#{args.inspect}) should have been #{assump.inspect}"
  end

  def assert_chain(assump, inst_sub, *meths)
    assert_equal( assump, meths.inject(instance_variable_get(inst_sub)) do |s,i|
      s.send(*i)
    end, 
    "#{inst_sub}.#{meths.join('.')} should have been #{assump.inspect}")
  end


  test "example given" do
    assert_chain 2, :@only_plan, :plan_steps, :size

    # attributes=, and then save() is 
    # an equivalent operation to update_attributes().
    # I only split them here to show the marked_for_destruction? portion.
    @only_plan.attributes= {
      :plan_steps_attributes =>
      {
        "0"=>{"number"=>"1", "id"=>@one.id.to_s, 
          "_destroy"=>"0", "description"=>"one"}, 
        "1"=>{"number"=>"2", "id"=>(@two.id + 1).to_s, 
          "_destroy"=>"0", "description"=>"three"}, 
        "2"=>{"id"=>@two.id.to_s, 
          "_destroy"=>"1"},
      }
    }

    #The validations of the _resulting_ affected records pass
    assert_chain true, :@only_plan, :errors, :empty? 
    @two_in_plan_steps = @only_plan.plan_steps.detect{|x| x.id == @two.id}
    assert_chain true, :@two_in_plan_steps, :marked_for_destruction?
    #Three was ignored because of the id

    assert_chain true, :@only_plan, :save 

    #The relevant records have been created and destroyed
    @plan_step_set = @only_plan.reload.plan_steps.reload.map{|i| 
      [i.description, i.number]}

    assert_chain true, :@two_in_plan_steps, :destroyed?

    assert_to [['one', 1]], :@plan_step_set, :sort 

    #removing the id makes it appear correctly
    assert_to( true, :@only_plan, :update_attributes, {
      :plan_steps_attributes =>
      {
        "1"=>{"number"=>"2", "_destroy"=>"0", "description"=>"three"}, 
      }
    }
    )

    @plan_step_set = @only_plan.reload.plan_steps.reload.map{|i| 
      [i.description, i.number]}

    assert_to [['one', 1], ['three', 2]], :@plan_step_set, :sort

  end
end

当然,给定的测试数据实际上根本没有使用您的验证。

很难确切地说出您希望验证的内容。 “每个PlanStep都有一个数字(表示'1st','2nd'等)”似乎表明你可能正试图在db('1st','2nd'等等中存储plan_steps的序数比'1st','3rd',其他一些独特的数字。)标准很难使用,而且方便,易于生成。只要您放入数据库的'数字'将按正确顺序放置行,您可以通过遍历after_initialize回调中的plan_steps集合,或者通过向关联添加mysql hacks来为它们分配序数。

但是您的示例数据和代码似乎表明不是这样,因此我们无法给您提供任何可靠的建议。

您是否尝试让用户对某些元素进行重新排序,在这种情况下,您可能需要上面的序数解决方案而不对位置进行任何验证(只需要很好的默认值以便新的PlanSteps将自己置于列表的末尾)或者'数字的重要性和重要性稀疏?

在客户制作和使用这些PlanSteps时,客户应该看到哪些条件(如果有)?