Neil Neil - 5 months ago 16
Ruby Question

Uniqueness Validation on has_many association records via accepts_nested_attributes_for

Here are my Models:

class User < ApplicationRecord
has_many :cities_users, inverse_of: :user
has_many :cities, through: :cities_users

accepts_nested_attributes_for :cities_users
end


class CitiesUser < ApplicationRecord
belongs_to :user
belongs_to :city

validates :user_id, uniqueness: { scope: :city_id, message: "already specified that they have lived in this city"}
end


class City < ApplicationRecord
has_many :cities_users
has_many :users, through: :cities_users
end


While creating a new
user
record: The user can dynamically add cities that they have lived in (via the nested_form_fields gem ). This ultimately creates records on the join table of
cities_users
.

clicking button

After clicking the 'Add a city you have lived in' button:

enter image description here

User then goes and clicks the button again to add another city:

click button again

That situation above is where I want a validation error to trigger when the user clicks the
Create User
button. When creating a new user record: a user should NOT specify that they lived in the same city more than one time.

So above: without a validation: two
cities_users
records would be created with the same
user_id
and
city_id
. This is no good, so I want to re-render the form, highlight the two offending
cities_users
nested_fields and have an error message say something like "You cannot specify that the user has lived in the same city more than once".

Clearly this requires a validation either on the
user
model or the
cities_user
model. I do not know where the validation should go, and I do not know how to code the validation so that it catches this error. The current
uniqueness
validation I have on the
CitiesUser
model does not catch this situation.

Extra setup information just in case someone wants to recreate the scenario

This was how I set up the nested_fields within the user
_form.html.erb
:

<%= f.nested_fields_for :cities_users do |ff| %>

<div>
<%= ff.label :city_id %>
<%= ff.collection_select(:city_id, City.all, :id, :name) %>
</div>

<% end %>

<br>

<div><%= f.add_nested_fields_link :cities_users, "Add a City You have lived in" %></div>


The
users_controller#create
action is standard, generated from the
scaffold
command. I did add to the strong params in order to allow for the nested attributes:

def user_params
params.require(:user).permit(:name, cities_users_attributes: [:id, :city_id, :user_id])
end

Answer

I finally got this to do the job:

#models/user.rb
class User < ActiveRecord::Base

  has_many :cities_users
  has_many :cities, through: :cities_users

  accepts_nested_attributes_for :cities_users, allow_destroy: true

  validate :no_duplicate_cities

  private

  def no_duplicate_cities
    errors.add(:duplicate_cities, "are present") if self.cities_users.group_by(&:city_id).values.detect{|arr| arr.size > 1}
  end
end