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 %>
  • Brad

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

  • http://www.bobrowski.org.pl Sebastian

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

    • admin

      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!

  • Kirk

    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

  • admin

    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.

  • http://new.techism.com Techism

    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 =&gt; 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(&amp;: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 =&gt; 3..40
      # validates_uniqueness_of   :login
      # validates_format_of       :login,    :with =&gt; Authentication.login_regex, :message =&gt; Authentication.bad_login_message
    
      validates_format_of       :name,     :with =&gt; Authentication.name_regex,  :message =&gt; Authentication.bad_name_message, :allow_nil =&gt; true
      validates_length_of       :name,     :maximum =&gt; 100
    
      validates_presence_of     :email
      validates_length_of       :email,    :within =&gt; 6..100 #r@a.wk
      validates_uniqueness_of   :email
      validates_format_of       :email,    :with =&gt; Authentication.email_regex, :message =&gt; 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 =&gt; {:email =&gt; email} # need to get the salt
        u &amp;&amp; u.authenticated?(password) ? u : nil
    
      end
    
      def self.legacy(email, password)
        u = LegacyUser.find(:first, :conditions =&gt; {:email =&gt; email, :password =&gt; 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 &lt; ActiveRecord::Base
      belongs_to :user
    end
    

    Views/Users/_form.html.erb

  • http://new.techism.com Techism

    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 =&gt; 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(&amp;: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 =&gt; 3..40
      # validates_uniqueness_of   :login
      # validates_format_of       :login,    :with =&gt; Authentication.login_regex, :message =&gt; Authentication.bad_login_message
    
      validates_format_of       :name,     :with =&gt; Authentication.name_regex,  :message =&gt; Authentication.bad_name_message, :allow_nil =&gt; true
      validates_length_of       :name,     :maximum =&gt; 100
    
      validates_presence_of     :email
      validates_length_of       :email,    :within =&gt; 6..100 #r@a.wk
      validates_uniqueness_of   :email
      validates_format_of       :email,    :with =&gt; Authentication.email_regex, :message =&gt; 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 =&gt; {:email =&gt; email} # need to get the salt
        u &amp;&amp; u.authenticated?(password) ? u : nil
    
      end
    
      def self.legacy(email, password)
        u = LegacyUser.find(:first, :conditions =&gt; {:email =&gt; email, :password =&gt; 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

  • admin

    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

  • http://new.techism.com Techism

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

    Good blog post though!

  • admin

    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

  • http://new.techism.com Techism

    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

  • http://new.techism.com Techism

    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?

  • http://seanbehan.com Sean Behan

    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.

  • Giovanni

    Thanks.