jdersen jdersen - 5 months ago 18
Ruby Question

Rails - Devise - Add profile information to separate table

I am using Devise to build a registration/authentication system into my application.

Having looked at quite a few resources for adding information to the devise model (e.g. username, biography, avatar URL, et cetera..) [resources include Jaco Pretorius' website, this (badly formed) SO question, and this SO question.

That's all fine and well -- it works. But my problem is that it's saving to the User model, which, according to database normalizations (also referencing this SO question), it should in fact be saving to a sub-model of User which is connected via

has_one
and
belongs_to
.

Thus far, I have created a
User
model via Devise. I have also created a
UserProfile
model via the
rails generate
script.

user.rb (for reference)

class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable

has_one :user_profile, dependent: :destroy
end


user_profile.rb

class UserProfile < ActiveRecord::Base
belongs_to :user
end


timestamp_create_user_profiles.rb

class CreateUserProfiles < ActiveRecord::Migration
def change
create_table :user_profiles do |t|
t.string :username, null: false
t.string :biography, default: ""

t.references :user, index: true, foreign_key: true

t.timestamps null: false
end
add_index :user_profiles, [:user_id, :username]
end
end


My question, now, is, how does one collect the information for both of these models and ensure, via the devise registration form, that it all ends up in the right places?

I've seen resources about creating state machines (AASM, and the answer to this SO question. I've also seen information about creating a wizard with WICKED, and an article on the same topic.

These all seem too complicated for my use-case. Is there some way to simply separate the inputs with devise and make sure the end up in the right place?

Answer

I think, instead of simply commenting on an answer that led me to the final answer, I'll archive the answer here in case someone in the future is trying to also find this answer:

I will be assuming that you have some sort of setup as I do above.

First step is you need to modify your User controller to accept_nested_attributes_for the profile reference as well as add a utility method to the model so when requested in code, the application can either retrieve the built profile model or build one.

The user model ends up looking like so:

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable

  has_one :user_profile, dependent: :destroy
  accepts_nested_attributes_for :user_profile

  def user_profile
    super || build_user_profile
  end
end

Secondly, you will need to modify your sign up/account_update form to be able to pass the attributes for this secondary model into the controller and eventually to be able to build the profile for the parent model.

You can do this by using f.fields_for.

Add something like this to your form:

<%= f.fields_for :user_profile do |user_profile_form| %>
 <%= user_profile_form.text_field :attribute %>
<% end %>

An example of this in my specific case is:

<%= f.fields_for :user_profile do |user_profile_form| %>
  <div class="form-group">
    <%= user_profile_form.text_field :username, class: "form-control", placeholder: "Username" %>
  </div>
<% end %>

Finally, you will need to tell Devise that it should accept this new hash of arguments and pass it to the model.

If you have created your own RegistrationsController and extended Devise's, it should look similar to this:

class RegistrationsController < Devise::RegistrationsController
  private
    def sign_up_params
      params.require(:user).permit(:email, :password, user_profile_attributes: :username)
    end
end

(Of course, make the proper changes for your specific use-case.)

If you have simply added the Devise sanitization methods to your application controller, it should look similar to this:

class ApplicationController < ActionController::Base
  before_filter :configure_permitted_parameters, if: :devise_controller?

  protected
    def configure_permitted_parameters
      devise_parameter_sanitizer.for(:sign_up) {|u|
        u.permit(:email, :password, user_profile_attributes: :username)}
    end
end

(Again, make the proper changes for your specific use-case.)

A small note on user_profile_attributes: :username: Note this is a hash, of course. If you have more than one attribute you are passing in, say, as an account_update (hint hint), you will need to pass them like so user_profile_attributes: [:attribute_1, :attribute_2, :attribute_3].