views:

196

answers:

1

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.

A: 

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...).

Ryan
Thanks for your input. I BELIEVE I've tried the validates_uniqueness_of already, and the problem was that at the time of validation, the conflicting step that is marked for deletion was not yet deleted, so a conflict existed. Regardless, I will try this a little later.As for collecting the number, it is managed by a bit of Javascript in hidden values. Users shouldn't be able to mess up the numbers unless they tamper with the inputs. Still, I don't think how I collect the data from the form will solve the problem I'm having here.
Well, because the position is always stored in a hidden value, and not liable to user error, then I would just give them all sequential values in the form and handle it on the backside. In your example, the "number" would be 1,2,3 with 2 scheduled to be deleted. Then you could call a PlanStep.reorder function that goes through and orders them now that the 2 is gone. Another option would just be to create new steps with the last number (Plan.plan_steps.size + 1) and let them use drag/drop to reorder. Again, I'm not looking at your project, this is just how I handle it in these cases.
Ryan
Now, assuming I do go the reordering route, my question is at what point do I do this reordering? Right now in my mind it looks like this: User input comes in -> Call "@plan.update_attributes(params[:plan])" -> Save/Delete steps -> Call reorder function, making another DB call to update the step numbers. I'm trying to avoid that second DB call because I have all the info and it should be possible at save time for the plan model, rather than afterwards. If I must do it afterwards, then I suppose I shall. Something feels wrong. Maybe I'm expecting too much from Rails.
There could be a better way, I don't really know. The way I typically handle this is to assign the location as the next in the list and let them reorder via drag/drop as needed (because that's what's worked best given the scenarios I've encountered). My suggestion at this point would be to make it work, mark it with a TODO note to look at later, and move on with your project. Better to make some progress and come back later after you've learned more. I'm always amazed at how much I learn over the course of every project.
Ryan
Agreed, and thanks for the help. I took the "move along" approach and I'll come back to it when the time is right. I will leave this question open and I'll answer it if I ever figure this out.