I’ve spent all day trying to get Devise and nested attributes to play nicely together. This and giving the user the ability to update parts of their profile without providing a password proved kind of tricky. Here’s how I got things working.
The code for this tutorial is on GitHub: https://github.com/hibbard-eu/authentication-with-devise-and-cancancan
I have a standard Devise User
model which contains of the usual email and password fields. The only deviation from Devise’s standard configuration is that a user has an associated profile which is specified using belongs_to
in the User
model (as this is where I want to place the foreign key). The profile will contain a bunch of information about the user and should be nested in the form which is shown when a user accesses Devise’s edit_user_registration_path
.
To start with lets create a new project and generate the resources:
rails new users && cd users
rails g scaffold User profile:belongs_to
rails g scaffold Profile name:string age:integer hobbies:text
rake db:migrate
Add the Devise gem to the Gemfile, run bundle, then initialize the gem:
gem 'devise'
bundle install
rails g devise:install
rails g devise User
rake db:migrate
Make the usual Devise specific configuration:
Add the following line to the bottom of config/environments/development.rb
:
config.action_mailer.default_url_options = {:host => 'localhost:3000'}
And set a root route in /config/routes.rb
root to: "welcome#index"
We’ll also need to create a WelcomeController
which contains an index action, as well as the corresponding view:
class WelcomeController < ApplicationController
def index
end
end
<h1>Welcome</h1>
<p>This is the welcome page</p>
Finally, add the following to layouts/application.html.erb:
<% if user_signed_in? %>
Signed in as <%= current_user.email %>
<%= link_to "Edit profile", edit_user_registration_path %> |
<%= link_to "Sign out", destroy_user_session_path, :method => :delete %>
<% else %>
<%= link_to "Sign up", new_user_registration_path %> or <%= link_to "sign in", new_user_session_path %>
<% end %>
<% flash.each do |name, msg| %>
<%= content_tag :div, msg, id: "flash_#{name}" %>
<% end %>
Before we can get on to customizing the “Edit profile” page, we’ll need to create a user and a profile, then associate the one with the other:
rails c
pw = "12345678"
p = Profile.create(name: "Jack")
u = User.create(email: "a@b.com", password: pw, password_confirmation: pw, profile: p)
Nested Attributes
Now if you go to http://localhost:3000
you should be able to sign in with the above credentials. Once you’ve done that, click the “Edit profile” link in the top right and you should see a form like this:
In order to customize this view, we need to have Devise copy a few files into our project:
rails g devise:views
And tell our User
model that we will be requiring nested attributes:
accepts_nested_attributes_for :profile
Now we can edit app/views/devise/registrations/edit.html.erb
:
<h2>Login Details</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= devise_error_messages! %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true %>
</div>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
<% end %>
<div class="field">
<%= f.label :password, "New password" %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div class="field">
<%= f.label :password_confirmation, "New password confirmation" %><br />
<%= f.password_field :password_confirmation, autocomplete: "off" %>
</div>
<div class="field">
<%= f.label :current_password %> <i>(to confirm changes)</i><br />
<%= f.password_field :current_password, autocomplete: "off" %>
</div>
<%= f.fields_for :profile do |builder| %>
<hr />
<h2>Profile Details</h2>
<div class="field">
<%= builder.label :name %><br />
<%= builder.text_field :name %>
</div>
<div class="field">
<%= builder.label :age %><br />
<%= builder.text_field :age %>
</div>
<div class="field">
<%= builder.label :hobbies %><br />
<%= builder.text_area :hobbies, :rows => 5 %><br />
</div>
<% end %>
<div class="actions">
<%= f.submit "Update" %>
</div>
<% end %>
<%= link_to "Back", :back %>
Now in order for Devise to register these changes at all, we need to update our ApplicationController
:
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:account_update) {|u| u.permit(
:email,
:password,
:password_confirmation,
:current_password,
profile_attributes: [:id, :name, :age, :hobbies]
)}
end
At this point if you attempt to edit the profile (including any of the nested attributes), it’ll work, but Devise will steadfastly insist on you entering a password. Let’s change that.
Allowing Users to Update Things Without a Password
In the last part of this post, I would like to demonstrate how to have Devise permit updates to any of the nested attributes without a user having to enter their password. However, it should still ask for password confirmation and/or a password if a user tries to update either the email
or password
fields.
The first step to achieving this is to alter our routes file:
devise_for :users, :controllers => {:registrations => "registrations"}
This overrides Devise’s RegistrationsController
and allows us to add our own logic to handle updates.
Now we need to create the file app/controllers/registrations_controller.rb
and add the following:
class RegistrationsController < Devise::RegistrationsController
def update
account_update_params = devise_parameter_sanitizer.sanitize(:account_update)
@user = User.find(current_user.id)
if needs_password?
successfully_updated = @user.update_with_password(account_update_params)
else
account_update_params.delete('password')
account_update_params.delete('password_confirmation')
account_update_params.delete('current_password')
successfully_updated = @user.update_attributes(account_update_params)
end
if successfully_updated
set_flash_message :notice, :updated
sign_in @user, :bypass => true
redirect_to edit_user_registration_path
else
render 'edit'
end
end
private
def needs_password?
@user.email != params[:user][:email] || params[:user][:password].present?
end
end
This checks to see if the user is attempting to update the email or password field. If so, and if validation passes, then it calls update_with_password which will carry out the necessary password checks, otherwise it calls update_attributes, which doesn’t.
And that’s everything.
The code for this tutorial is on GitHub: https://github.com/hibbard-eu/authentication-with-devise-and-cancancan
Hey thanks for the tutorial.. was having a similar problem and this sort of helped! Had one problem tho.. the solution only works if you create both the related objects in console and manually link them.
As I only needed the nested attributes for update too, i had to add the method create in the registration controller:
without this the
fields_for
won’t show in the edit page.