Вложенные атрибуты Rails и пользовательская проверка - PullRequest
1 голос
/ 02 сентября 2010

Я довольно новичок в Ruby и Rails (использую 2.3.8), так что простите, если я упустил что-то действительно очевидное, но я некоторое время боролся с этим, и мои поиски были бесплодны.

В моем коде у меня есть планы, а у плана много плановых шагов.Каждый 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 /plan_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 и создать новую запись (ID = 3) с номером = 2.

ОК, так что с этой настройкой, вот моя проблема:

Когда я вызываю plan_steps при проверке, кажется, что он извлекает значения из базы данных, а не из массива params [], передаваемого в update_attributes.

Например, если я раскомментирую строку «Put» в проверке, я вижу описания Plan_Steps в том виде, в котором они существуют в базе данных, а не в том, что они существуют в переданных параметрах.Это означает, что я не могу проверить входящие Plan_Steps.

Я также не могу выполнить проверку в модели Plan_Steps, поскольку, если я не ошибаюсь, проверка будет происходить с базой данных (а не с параметрами, переданными в).

Прошу прощения, если это плохо сформулированный вопрос, но он довольно конкретный.Если вам нужны какие-либо разъяснения, пожалуйста, спросите.

И помните, я новичок, так что я мог легко совершить действительно глупую ошибку.

Ответы [ 2 ]

1 голос
/ 02 сентября 2010

Насколько я знаю, любая проверка, которую вы проводите в модели, будет проверять базу данных.Если вы хотите сравнить значения в параметрах, вам нужно сделать это, прежде чем вы достигнете валидации БД (не рекомендуется вообще).Кроме того, просто для справки в будущем, ваша валидация может быть достигнута с помощью встроенного validates_uniqueness_of следующим образом:

validates_uniqueness_of :number, :scope => :plan_id

Что касается того, что вы пытаетесь достичь в конце (и имейте в виду, я неЯ не очень разбираюсь в вашем проекте, поэтому возьмите его с собой. Я бы порекомендовал рассчитать позицию шага на сервере, а не полагаться на пользовательский ввод.Я бы сделал конкретные предложения, но сложно сказать, не зная, как вы собираете свое «числовое» значение (перетаскивание, ручной ввод, расположение в списке и т. Д.).

0 голосов
/ 01 декабря 2010

433887,

Я написал несколько тестов для вашего вопроса, так как сам не был уверен в том, как acceptpts_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 имеет номер (для обозначения« 1-й »,« 2-й »и т. Д.)» Может показаться, что, возможно, вы пытаетесь сохранить порядковые номера для plan_steps в БД («1-й», «2-й» и т. Д. чем «1-й», «3-й», какой-то другой уникальный номер.) С ординалами трудно работать, и их удобно создавать. Пока числа, которые вы вводите в базу данных, будут располагать строки в правильном порядке, вы можете назначать им порядковые номера, просматривая набор plan_steps в обратном вызове after_initialize или добавляя mysql hacks в ассоциацию.

Но данные вашего примера и код, похоже, указывают на обратное, поэтому на самом деле мы не можем дать вам какой-либо убедительный совет.

Пытаетесь ли вы позволить пользователю переупорядочить некоторые элементы, и в этом случае вы, вероятно, захотите описанное выше порядковое решение без какой-либо проверки позиции (просто хорошие значения по умолчанию, чтобы новые PlanSteps помещали себя в конец списка), или число значимо, а главное, редко?

При каких условиях, если таковые имеются, ваши клиенты должны видеть ошибки при совершении и использовании этих PlanSteps?

...