Nested Has_one Relationship with Fields_for and Attr_accessible in Model Class

To make child attributes accessible to your model through a nested forms (Rails 2.3) you’ll need to add the “#{child_class}_attributes” to the attr_accessible method in your parent class. If you don’t use attr_accessible in your parent model (you would do this to restrict certain attributes to be accessed via a web form) then you should be all set.

Below is an example where User has_one Profile with the favorite_color attribute being set/updated in the nested form.

class User < ActiveRecord::Base
  has_one :profile #child class
  accepts_nested_attributes_for :profile
  attr_accessible :profile_attributes # the format is the child_class followed by the "_attributes"
end

And the form would like this...

<% form_for @current_user do |f| %>
   <% f.fields_for :profile do |profile| %>
     <%= profile.text_field :favorite_color %>
  <% end %>
<% end %>

Thank you! Saved me from having to read the API docs! +1 internets for you, sir!

I cant see your code in Safari, in FF is ok

Thanks for the heads up. Not sure what the problem is but I’ll take a look at it. It might be the “” characters aren’t being escaped properly!

Hi, I thank you for your infromation here.

For some reason after following the information here in a similar User-Profile thing, it still says WARNING: Can’t mass-assign these protected attributes: profile

I’m using Restful authentication User model for this, im wondering if there is other stuff going on?

Any ideas?

Thanks

With restful authentication there is a chunk of code in the user model

  # HACK HACK HACK -- how to do attr_accessible from here?
  # prevents a user from submitting a crafted form that bypasses activation
  # anything else you want your user to change should be added here.
  attr_accessible :login, :email, :name, :password, :password_confirmation

Try adding :profile to the attr_accessible list or… commenting out the attr_accessible line altogether. You can use attr_protected instead and explicitly list attributes unavailable for mas assignment. A blacklist as opposed to a whitelist approach to protecting your model attributes.

I’m having a problem getting this to to work with Restful Authentication too…my error is “undefined method `fields_for’ for nil:NilClass”. I’ve checked, checked, and checked my code again, but can’t see what’s causing this problem!

User model:

require 'digest/sha1'

class User  :destroy
  #accepts_nested_attributes_for :profile, :allow_destroy => true

  # has_role? simply needs to return true or false whether a user has a role or not.
  # It may be a good idea to have "admin" roles return true always
  def has_role?(role_in_question)
    @_list ||= self.roles.collect(&:name)
    return true if @_list.include?("admin")
    (@_list.include?(role_in_question.to_s) )
  end
  # ---------------------------------------
  include Authentication
  include Authentication::ByPassword
  include Authentication::ByCookieToken
  include Authorization::StatefulRoles
  # validates_presence_of     :login
  # validates_length_of       :login,    :within => 3..40
  # validates_uniqueness_of   :login
  # validates_format_of       :login,    :with => Authentication.login_regex, :message => Authentication.bad_login_message

  validates_format_of       :name,     :with => Authentication.name_regex,  :message => Authentication.bad_name_message, :allow_nil => true
  validates_length_of       :name,     :maximum => 100

  validates_presence_of     :email
  validates_length_of       :email,    :within => 6..100 #r@a.wk
  validates_uniqueness_of   :email
  validates_format_of       :email,    :with => Authentication.email_regex, :message => Authentication.bad_email_message

  #validates_presence_of     :company, :address, :country, :company_type, :phone, :fax, :end_product, :end_product_use, :end_product_categories, :end_user_countries, :statement_name, :statement_signature, :statement_title

  # HACK HACK HACK -- how to do attr_accessible from here?
  # prevents a user from submitting a crafted form that bypasses activation
  # anything else you want your user to change should be added here.
  attr_accessible :profile_attributes, :profile, :login, :email, :name, :password, :password_confirmation#, :company, :address, :country, :company_type, :phone, :fax, :end_product, :end_product_use, :end_product_categories, :end_user_countries, :statement_name, :statement_signature, :statement_title, :end_use_production_date, :website, :ultimate_consignee_name, :ultimate_consignee_address, :ultimate_consignee_type, :ultimate_consignee_website

  # Authenticates a user by their login name and unencrypted password.  Returns the user or nil.
  #
  # uff.  this is really an authorization, not authentication routine.
  # We really need a Dispatch Chain here or something.
  # This will also let us return a human error message.
  #
  def self.authenticate(email, password)
    u = find_in_state :first, :active, :conditions => {:email => email} # need to get the salt
    u && u.authenticated?(password) ? u : nil

  end

  def self.legacy(email, password)
    u = LegacyUser.find(:first, :conditions => {:email => email, :password => password})
    u ? u : nil
  end

  def login=(value)
    write_attribute :login, (value ? value.downcase : nil)
  end

  def email=(value)
    write_attribute :email, (value ? value.downcase : nil)
  end

  protected

    def make_activation_code
        self.deleted_at = nil
        self.activation_code = self.class.make_token
    end

end

Profile Model:

class Profile < ActiveRecord::Base
  belongs_to :user
end

Views/Users/_form.html.erb

Huh…it scrambled my comment and changed my code in strange places. Let me try posting that code again:

User Model

require 'digest/sha1'

class User  :destroy
  #accepts_nested_attributes_for :profile, :allow_destroy => true

  # has_role? simply needs to return true or false whether a user has a role or not.
  # It may be a good idea to have "admin" roles return true always
  def has_role?(role_in_question)
    @_list ||= self.roles.collect(&:name)
    return true if @_list.include?("admin")
    (@_list.include?(role_in_question.to_s) )
  end
  # ---------------------------------------
  include Authentication
  include Authentication::ByPassword
  include Authentication::ByCookieToken
  include Authorization::StatefulRoles
  # validates_presence_of     :login
  # validates_length_of       :login,    :within => 3..40
  # validates_uniqueness_of   :login
  # validates_format_of       :login,    :with => Authentication.login_regex, :message => Authentication.bad_login_message

  validates_format_of       :name,     :with => Authentication.name_regex,  :message => Authentication.bad_name_message, :allow_nil => true
  validates_length_of       :name,     :maximum => 100

  validates_presence_of     :email
  validates_length_of       :email,    :within => 6..100 #r@a.wk
  validates_uniqueness_of   :email
  validates_format_of       :email,    :with => Authentication.email_regex, :message => Authentication.bad_email_message

  #validates_presence_of     :company, :address, :country, :company_type, :phone, :fax, :end_product, :end_product_use, :end_product_categories, :end_user_countries, :statement_name, :statement_signature, :statement_title

  # HACK HACK HACK -- how to do attr_accessible from here?
  # prevents a user from submitting a crafted form that bypasses activation
  # anything else you want your user to change should be added here.
  attr_accessible :profile_attributes, :profile, :login, :email, :name, :password, :password_confirmation#, :company, :address, :country, :company_type, :phone, :fax, :end_product, :end_product_use, :end_product_categories, :end_user_countries, :statement_name, :statement_signature, :statement_title, :end_use_production_date, :website, :ultimate_consignee_name, :ultimate_consignee_address, :ultimate_consignee_type, :ultimate_consignee_website

  # Authenticates a user by their login name and unencrypted password.  Returns the user or nil.
  #
  # uff.  this is really an authorization, not authentication routine.
  # We really need a Dispatch Chain here or something.
  # This will also let us return a human error message.
  #
  def self.authenticate(email, password)
    u = find_in_state :first, :active, :conditions => {:email => email} # need to get the salt
    u && u.authenticated?(password) ? u : nil

  end

  def self.legacy(email, password)
    u = LegacyUser.find(:first, :conditions => {:email => email, :password => password})
    u ? u : nil
  end

  def login=(value)
    write_attribute :login, (value ? value.downcase : nil)
  end

  def email=(value)
    write_attribute :email, (value ? value.downcase : nil)
  end

  protected

    def make_activation_code
        self.deleted_at = nil
        self.activation_code = self.class.make_token
    end

end

Views/Users/_form.html.erb

The fields_for method is not on the model… it is a form helper method. what does your form/view look like?

use the “pre” tag instead of code… but if you have a lot of code and code that uses < and > characters, it’s easier to use a pastie http://pastie.org and share the link

Hmm…Never mind. I figured out what was wrong and couldn’t post my code anyway for some reason.

Good blog post though!

Sorry about the code not posting… could be the < and > tags are being stripped… form views/erb often starts w/ those. glad to hear it’s working though

Well…turns out it just SEEMED to be working. I get the form, validations work, but when they are all passed and the @user is saved, the profile_id is blank and no profile is saved. Quite a mystery to me.

I’ll try to share my code with “pre” tags here. If that fails, I try that pastie solution…

# in models/user.rb
require ‘digest/sha1′

class User :destroy
accepts_nested_attributes_for :profile, :allow_destroy => true

# has_role? simply needs to return true or false whether a user has a role or not.
# It may be a good idea to have “admin” roles return true always
def has_role?(role_in_question)
@_list ||= self.roles.collect(&:name)
return true if @_list.include?(“admin”)
(@_list.include?(role_in_question.to_s) )
end
# —————————————
include Authentication
include Authentication::ByPassword
include Authentication::ByCookieToken
include Authorization::StatefulRoles
# validates_presence_of :login
# validates_length_of :login, :within => 3..40
# validates_uniqueness_of :login
# validates_format_of :login, :with => Authentication.login_regex, :message => Authentication.bad_login_message

validates_format_of :name, :with => Authentication.name_regex, :message => Authentication.bad_name_message, :allow_nil => true
validates_length_of :name, :maximum => 100

validates_presence_of :email
validates_length_of :email, :within => 6..100 #r@a.wk
validates_uniqueness_of :email
validates_format_of :email, :with => Authentication.email_regex, :message => Authentication.bad_email_message

#validates_presence_of :company, :address, :country, :company_type, :phone, :fax, :end_product, :end_product_use, :end_product_categories, :end_user_countries, :statement_name, :statement_signature, :statement_title

# HACK HACK HACK — how to do attr_accessible from here?
# prevents a user from submitting a crafted form that bypasses activation
# anything else you want your user to change should be added here.
#attr_accessible :profile_attributes, :profile, :login, :email, :name, :password, :password_confirmation#, :company, :address, :country, :company_type, :phone, :fax, :end_product, :end_product_use, :end_product_categories, :end_user_countries, :statement_name, :statement_signature, :statement_title, :end_use_production_date, :website, :ultimate_consignee_name, :ultimate_consignee_address, :ultimate_consignee_type, :ultimate_consignee_website

# Authenticates a user by their login name and unencrypted password. Returns the user or nil.
#
# uff. this is really an authorization, not authentication routine.
# We really need a Dispatch Chain here or something.
# This will also let us return a human error message.
#
def self.authenticate(email, password)
u = find_in_state :first, :active, :conditions => {:email => email} # need to get the salt
u && u.authenticated?(password) ? u : nil
end

def self.legacy(email, password)
u = LegacyUser.find(:first, :conditions => {:email => email, :password => password})
u ? u : nil
end

def login=(value)
write_attribute :login, (value ? value.downcase : nil)
end

def email=(value)
write_attribute :email, (value ? value.downcase : nil)
end

protected

def make_activation_code
self.deleted_at = nil
self.activation_code = self.class.make_token
end
end

# in models/profile.rb
class Profile < ActiveRecord::Base
belongs_to :user
validates_presence_of :address, :company,
:country, :company_type, :phone,
:statement_name, :statement_signature, :statement_title,
:end_product_use, :end_use_production_date, :end_product_categories

# def accessible_to(user_id)
# if self.user.id == user_id || admin?
# return true
# end
# end

end

# in views/users/_form.html.erb

{:action => ‘create’} do |f| %>

“/profiles/form”, :locals=>{:profile_form => profile_form } %>

#in controllers/users_controller.rb
def new
@user = User.new
@user.build_profile
end

Hmmm. Sorry for choding up your comments with this…looks like pastie for me. (neat resource BTW!)

Can’t seem to post the script or links, so?

Here is another example model using restful authentication http://pasite.org/code/569 w/all validations removed. to debug, you may want to comment out all of the validations in your model and focus on the root of the problem. remove anything that you see as extraneous to the bug.

looking at your code again, i don’t see the

has_one :profile

declaration. you need this to make the association work, which would explain why the profile_id is failing to be populated.

the important parts are

has_one :profile
accepts_nested_attributes_for :profile
attr_accessible :profile_attributes

# – minus < & > tags for this erb code
f.fields_for :profile do |profile|
profile.text_field :favorite_color %>
end

thanks for the comments… i enjoy them. also, you may want to take a look at AuthLogic. It is the new standard for Rails authentication plugins. http://railscasts.com/episodes/160-authlogic Ryan Bates has a great screencast for setting it up.

 
*name

*e-mail

web site

leave a comment