Rails 嵌套属性和自定义验证

发布于 2024-09-16 18:56:21 字数 2209 浏览 2 评论 0原文

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

在我的代码中,我有计划,并且一个计划有许多 Plan_Steps。每个 Plan_Step 都有一个编号(表示“第 1 个”、“第 2 个”等)。我有一个用于更新计划的表格,并且我需要验证每个 Plan_Step 都有唯一的编号。下面的代码可能会更好地解释设计:

models/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

controllers/plans_controller.rb:

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

现在,当我的表单提交更新时,参数哈希如下所示:

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

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

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

I'm fairly new to both Ruby and Rails (using 2.3.8), so forgive me if I'm missing something really obvious here but I've been struggling with this for a while and my searches have been fruitless.

In my code I have Plans, and a Plan has many Plan_Steps. Each Plan_Step has a number (to indicate '1st', '2nd', etc). I have a form to update a Plan, and I need to validate that each Plan_Step has a unique number. The code below might give a better explanation of the design:

models/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

controllers/plans_controller.rb:

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

Now when my form submits an update, the params hash looks like this:

  {"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"}

The database contains entries for Plan_Steps with the following:

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

Notice that ID=2 exists with Number=2, and what I'm trying to do is delete ID=2 and create a new entry (ID=3) with Number=2.

OK, so with that set up, here is my problem:

When I call plan_steps in the validation, it appears to be pulling the values from the database instead of from the params[] array passed to update_attributes.

For example, if I uncomment the 'puts' line in the validation, I see the descriptions of the Plan_Steps as they exist in the database, not as they exist from the passed-in parameters. This means I can't validate the incoming Plan_Steps.

I can't do validation in the Plan_Steps model, either, since unless I'm mistaken the validation will occur against the database (and not the parameters passed in).

I apologize if this is a poorly worded question, but it's fairly specific. If you need any clarification, please ask.

And remember, I'm a noob, so I could easily be making some really stupid mistake.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

楠木可依 2024-09-23 18:56:21

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

validates_uniqueness_of :number, :scope => :plan_id

至于您最终想要完成的任务(请记住,我对您的项目不太了解,所以请采取(对此持保留态度),我建议计算后端的步骤位置,而不是依赖用户输入。我会提出具体的建议,但如果不知道如何收集“数字”值(拖放、手动输入、列表位置等),就很难说。

Any validation you perform in the model is going to look at the database, as far as I know. If you want to compare the values in the params, you'll need to do so before you reach the db validations (not recommended at all). Also, just for future reference, your validation can be achieved using the built in validates_uniqueness_of like this:

validates_uniqueness_of :number, :scope => :plan_id

As for what you're trying to get accomplished in the end (and keep in mind I don't know much about your project, so take this with a grain of salt), I'd recommend calculating the step position on the back-end instead of relying on user input. I'd make specific suggestions, but it's tough to say without knowing how your collecting your "number" value (drag/drop, manual entry, list location, etc...).

晒暮凉 2024-09-23 18:56:21

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’等)”似乎表明您可能正在尝试在数据库中存储 plan_steps 的序数(‘1st’、‘2nd’等)序数很难使用,但很容易生成。只要您放入数据库的“数字”会将行按正确的顺序排列,您就可以通过在 after_initialize 回调中遍历 plan_steps 集来为它们分配序号,或者通过添加 mysql hacks 到该协会。

但您的示例数据和代码似乎表明情况并非如此,因此我们实际上无法为您提供任何可靠的建议。

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

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

433887,

I wrote some tests for your question, as I was unsure myself of how accepts_nested_attributes was working internally. There was a gotcha where records that did not exist would be silently ignored if they contained an 'id' attribute in the params that were passed in. See below.

#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

Of course, the given test data doesn't actually use your validation at all as written.

It's hard to tell exactly what you would want the validation to do. "Each PlanStep has a number (to indicate '1st', '2nd', etc)" would seem to indicate that maybe you're trying to store ordinals for the plan_steps in the db ('1st', '2nd', etc rather than '1st', '3rd', some other unique number.) Ordinals are hard to work with, and conveniently, easy to generate. As long as the 'number's you're putting into the database will put the rows in the correct order, you can assign them ordinals by walking through the set of plan_steps in an after_initialize callback, or though adding mysql hacks to the association.

But your example data and code seems to indicate otherwise, so there's not really any solid advice we can give you.

Are you trying to let the user reorder some elements, in which case you probably want the ordinal solution above without any validation on the position (just good defaults so that new PlanSteps place themselves at the end of the list,) or are the 'number's significant and importantly sparse?

Under what, if any, conditions should your customers see errors when they're making and using these PlanSteps?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文