1

我对 Ruby 和 Rails(使用 2.3.8)都很陌生,所以如果我在这里遗漏了一些非常明显的东西,请原谅我,但我已经为此苦苦挣扎了一段时间,而且我的搜索没有结果。

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

模型/计划.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 模型中进行验证,因为除非我弄错了,否则验证将针对数据库(而不是传入的参数)进行。

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

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

4

2 回答 2

1

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

validates_uniqueness_of :number, :scope => :plan_id

至于您最终要完成的工作(请记住,我对您的项目知之甚少,因此请谨慎对待),我建议您计算后面的台阶位置- end 而不是依赖用户输入。我会提出具体的建议,但如果不知道如何收集“数字”值(拖放、手动输入、列表位置等),就很难说。

于 2010-09-02T07:32:17.513 回答
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 中存储 plan_steps 的序数('1st'、'2nd' 等而不是比 '1st', '3rd', 其他一些唯一的数字。)序数很难使用,而且很方便,很容易生成。只要您放入数据库的“数字”将使行以正确的顺序排列,您可以通过在 after_initialize 回调中遍历 plan_steps 集或通过将mysql hacks添加到关联来为它们分配序数。

但是您的示例数据和代码似乎另有说明,因此我们实际上无法为您提供任何可靠的建议。

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

您的客户在制作和使用这些 PlanSteps 时会在什么情况下看到错误(如果有的话)?

于 2010-12-01T08:52:46.903 回答