Aug 13, 2014

Devise Tricks - Adding a Nested Model

blogpost author photo
Marek Stanisz
blogpost cover image
Devise doesn’t need much introduction – we all use it. It’s something I would call an industry standard for authentication. It works out of the box – five minutes and boom! You’ve got the basics of what you want and what you’ll ever need. More than that – it happens to be customizable. But this requires you to learn some tricks.

First of all – I assume that if you’re working with Ruby on Rails, you are familiar with basics of Object Oriented Programming (OOP for short). This includes overriding some methods. It’s pretty useful, because we don’t have direct access to Devise’s controllers, for example. When you have to customize the parameters that are saved in the model during account registration or edition, this is the only way to go.

The basics of this are covered in the Devise manual, so there’s no real need for me to cover all these mechanics or how they work. Instead, we’ll do something much more interesting, though not as hard as it may seem – we’ll add a nested model during the account edit action. Ready? Let’s get going!

I was adding the Blog model to the User model. A User could only have one blog. It was logical, then, that my User model should contain these lines:

has_one :blog
  accepts_nested_attributes_for :blog

Of course, the Blog model should contain the corresponding belongs_to :user line.

Next, it’s time to generate the default views (using rails g devise_views -v registrations command – this will be enough for our purposes) and modify them to our needs. To make things easier, let’s go ahead and get Ryan Bates’ nested_form gem. To set it up, just follow the instructions in the github manual. This little tool eases building nested forms A LOT! After all these operations, my haml looked like this:

= nested_form_for( [resource_name, resource.with_blog], url: registration_path(resource_name), html: { method: :put }) do |f|
    = devise_error_messages!
      = f.label :email
      = f.email_field :email, autofocus: true
      - if devise_mapping.confirmable? && resource.pending_reconfirmation?
          Currently waiting confirmation for: #{resource.unconfirmed_email}
      - if
        %h4 Add a blog
          = f.fields_for :blog, html: { method: :put } do |blog_form|
            = blog_form.label :url, 'Blog url'
            = blog_form.text_field :url
      - else
        %h4 Your blog
      = # passwords and stuff

It’s almost done, but it won’t work yet. By now, it’s time to permit the parameters in the controller. You can do it by overriding the Devise’s registrations controller, or you can choose the lazy way™ and permit it in your application controller.

Please keep in mind, that the second option may not be so good in terms of code optimization – it will check if we are inside the devise controller on every action someone performs. These lines added to your Application Controller should do the trick.

efore_action :update_sanitized_params, if: :devise_controller?

  def update_sanitized_params
    devise_parameter_sanitizer.for(:account_update) do |u|
      u.permit(:email, :password, :password_confirmation,
               :current_password, blog_attributes: [:url])

One more thing. We need to add the Blog builder to our User model. Nothing simpler.

def with_blog
    build_blog if blog.nil?

And that’s all – Devise handles the rest of the operations. Wasn’t that complicated at all, but took me some time on the Net to figure it all out. Hope you’ve enjoyed it. Now go and make some nested models yourself :)