In my Rails app I have Users. Users are asked for their home city and district / neighborhood.
class User < ActiveRecord::Base
belongs_to :city
belongs_to :district
end
class City < ActiveRecord::Base
has_many :users
has_many :districts
end
class District < ActiveRecord::Base
has_many :users
belongs_to :city
end
In forms I build the associations using a virtual attribute on the User model that accepts a string (more info below in case it’s relevant).
In the console this all works great, but in the UI it’s not working. The problem seems to be, I can get city_name through a form no problem, but when I try to assign city and district in the same form it always fails. In other words, mass assignment doesn’t work.
@user.update_attributes(params[:user])
Instead the only thing I have been able to figure out is to manually set each key from a form submission, like:
@user.name = params[:user][:name] if params[:user][:name]
@user.city_name = params[:user][:city_name] if params[:user][:city_name]
@user.district_name = params[:user][:district_name] if params[:user][:district_name]
This approach works, but it’s a pain, kind of brittle, and feels all wrong because it starts gunking the controller up with a lot of logic.
My question is:
-
Is there a way to create or update attributes in a specific order, ideally in the model so that the controller doesn’t have to worry about all this?
-
Am I doing this wrong? If so, what approach would be better.
Extra Info
Here’s how I build the associations using virtual attributes on the user model, in case that’s relevant to any potential answerers:
I want users to be able to select a city by just typing in a name, like “Chicago, IL”. This works fine, using a virtual attribute on the user model like so:
def city_name
city.try :full_name
end
def city_name=(string)
self.city = City.find_or_create_by_location_string( string )
end
It only makes sense for a user to find or create a district from the city they’ve chosen. This works slightly differently:
def district_name
district.try :name
end
def district_name=(string)
if self.city.nil?
raise "Cannot assign a district without first assigning a city."
else
self.district = self.city.districts.find_or_create_by_name( string )
end
end
In the model layer these things work fine, as long as both a city_name and district_name are set the district association works as expected.
I think you could do a few things to clean this up. First, you can use delegates to clean up the code. e.g.
that way you can do something like
and it will just work.
Next, I would say take the name-to-id logic out of your model. You can do it either in the form (e.g. an ajax search puts the district id/city id into a hidden field), or in the controller. Then just assign the city/district as normal.