Search code examples
ruby-on-rails-4nested-formsnested-attributesstrong-parametersbelongs-to

Rails 4 nested form with belongs_to problems saving


I'm working on a new Rails 4 application and having a hard time getting all the pieces to work together correctly. I have a model (sample) with a belongs_to association with another model (lat_long) and am trying to do CRUD operations on both from the sample views (editing the default generated views). The problem is that lat_long can be blank; everything works fine if it isn't but when it is it's hitting the validation in the model, so I think it's trying to save a blank lat_long instead of setting it to nil. update and edit are the problems.

Here are the relevant pieces from the code:
Models:

class Sample < ActiveRecord::Base
  belongs_to :lat_long
  accepts_nested_attributes_for :lat_long 
end

class LatLong < ActiveRecord::Base
  validates_each :west_longitude do |record, attr, value|
    record.errors.add attr, "must be within range." if (value < 110.0 or value > 115.0)   
  end   
  validates_each :north_latitude do |record, attr, value|
    record.errors.add attr, "must be within range." if (value < 38.0 or value > 42.0)   
  end 
end

samples/_form.html.erb used in edit and new:

<%= form_for(@sample) do |f| %>
    <%= f.fields_for :lat_long do |g| %>
        <div class="field">
          <%= g.label :north_latitude %><br>
          <%= g.text_field :north_latitude %>
        </div>
        <div class="field">
          <%= g.label :west_longitude %><br>
          <%= g.text_field :west_longitude %>
        </div>
    <% end %>
<% end %>

samples_controller.rb:

class SamplesController < ApplicationController
 # GET /samples/new
  def new
    @sample = Sample.new
    @sample.build_lat_long
    @sample.build_trap
  end
  # GET /samples/1/edit
  def edit
    @sample.lat_long || @sample.build_lat_long
    @sample.trap || @sample.build_trap
  end
  # POST /samples
  # POST /samples.json
  def create
    @sample = Sample.new(sample_params)
    puts sample_params
    respond_to do |format|
      if @sample.save
        format.html { redirect_to @sample, notice: 'Sample was successfully created.' }
        format.json { render :show, status: :created, location: @sample }
      else
        format.html { render :new }
        format.json { render json: @sample.errors, status: :unprocessable_entity }
      end
    end
  end
  # PATCH/PUT /samples/1
  # PATCH/PUT /samples/1.json
  def update
    respond_to do |format|
      if @sample.update(sample_params)
        format.html { redirect_to @sample, notice: 'Sample was successfully updated.' }
        format.json { render :show, status: :ok, location: @sample }
      else
        format.html { render :edit }
        format.json { render json: @sample.errors, status: :unprocessable_entity }
      end
    end
  end
  def sample_params
    params.require(:sample).permit(...,
                                   lat_long_attributes: [:id,
                                   :west_longitude, :north_latitude])
  end
end

I can do a check in the controller before updating of course, but is that the right thing to do? It "feels" like there ought to be a way to have Rails take care of this, but looking at lots of StackOverflow questions and the documentation haven't given me any ideas. Most examples used has_one or has_many, so I don't know if that's the problem? According to this question it's newish that belongs_to works with accepts_nested_attributes_for so perhaps it's just plain not set up for this.

I would like to know what the best practice is for this situation, the most "Railsy" thing to do. Oh and I think I've included all the relevant bits of code but if there's anything else you need to see let me know.


Solution

  • I'd do something like:

    class Sample < ActiveRecord::Base
      belongs_to :lat_long
      accepts_nested_attributes_for :lat_long, reject_if: proc { |attributes| attributes['west_longitude'].blank? && attributes['north_latitude'].blank? }
    end
    
    class LatLong < ActiveRecord::Base
      validates :west_longitude, numericality: { greater_than_or_equal_to: 110,
                                 less_than_or_equal_to: 115,
                                 message: "must be within range" },
                                 allow_nil: true
    
      validates :north_latitude, numericality: { greater_than_or_equal_to: 38,
                                 less_than_or_equal_to: 42,
                                 message: "must be within range" },
                                 allow_nil: true
    end