19

I'm working with Rails 3.1.0 and Devise 1.4.8, and am new to both.

I want to allow multiple users for an account. The first user to sign up creates the account (probably for their company), then that user can add more users. A user is always linked to exactly one account.

I have Users and Accounts tables. Abbreviated models:

class User < ActiveRecord::Base
  belongs_to :account
  devise :database_authenticatable, :registerable,
     :recoverable, :rememberable, :trackable, :validatable,
     :confirmable, :lockable, :timeoutable
  attr_accessible :email, :password, :password_confirmation, :remember_me
end

class Account < ActiveRecord::Base
  has_many :users, :dependent => :destroy
  attr_accessible :name, :account_type
end

The question is, when the first user signs up, how do I create both the Account and User?

  • Do I need to modify/override the Devise registrations_controller, something like the answer here? I couldn't figure out how to create the Account then pass it to Devise for creating the User.

  • account_id is already in the User model. Should I add account_name and account_type to the User model, and create a new the Account record if account_id is not passed in? In this case, I'm trying to hide Account creation from Devise, but not sure that will work since I still need to prompt for account_name and account_type on the registration page.

Community
  • 1
  • 1
Mark Berry
  • 17,843
  • 4
  • 58
  • 88

3 Answers3

25

Finally got this to work using nested attributes. As discussed in the comments on Kenton's answer, that example is reversed. If you want multiple users per account, you have to create the Account first, then the User--even if you only create one user to start with. Then you write your own Accounts controller and view, bypassing the Devise view. The Devise functionality for sending confirmation emails etc. still seems to work if you just create a user directly, i.e. that functionality must be part of automagical stuff in the Devise model; it doesn't require using the Devise controller.

Excerpts from the relevant files:

Models in app/models

class Account < ActiveRecord::Base
  has_many :users, :inverse_of => :account, :dependent => :destroy
  accepts_nested_attributes_for :users
  attr_accessible :name, :users_attributes
end

class User < ActiveRecord::Base
  belongs_to :account, :inverse_of => :users
  validates :account, :presence => true
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable
  attr_accessible :email, :password, :password_confirmation, :remember_me
end

spec/models/account_spec.rb RSpec model test

it "should create account AND user through accepts_nested_attributes_for" do
  @AccountWithUser = { :name => "Test Account with User", 
                       :users_attributes => [ { :email => "user@example.com", 
                                                :password => "testpass", 
                                                :password_confirmation => "testpass" } ] }
  au = Account.create!(@AccountWithUser)
  au.id.should_not be_nil
  au.users[0].id.should_not be_nil
  au.users[0].account.should == au
  au.users[0].account_id.should == au.id
end

config/routes.rb

  resources :accounts, :only => [:index, :new, :create, :destroy]

controllers/accounts_controller.rb

class AccountsController < ApplicationController

  def new
    @account = Account.new
    @account.users.build # build a blank user or the child form won't display
  end

  def create
    @account = Account.new(params[:account])
    if @account.save
      flash[:success] = "Account created"
      redirect_to accounts_path
    else
      render 'new'
    end
  end

end

views/accounts/new.html.erb view

<h2>Create Account</h2>

<%= form_for(@account) do |f| %>
  <%= render 'shared/error_messages', :object => f.object %>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>

  <%= f.fields_for :users do |user_form| %>
    <div class="field"><%= user_form.label :email %><br />
    <%= user_form.email_field :email %></div>
    <div class="field"><%= user_form.label :password %><br />
    <%= user_form.password_field :password %></div>
    <div class="field"><%= user_form.label :password_confirmation %><br />
    <%= user_form.password_field :password_confirmation %></div>
  <% end %>

  <div class="actions">
    <%= f.submit "Create account" %>
  </div>
<% end %>

Rails is quite picky about plural vs. singular. Since we say Account has_many Users:

  • it expects users_attributes (not user_attributes) in the model and tests
  • it expects an array of hashes for the test, even if there is only one element in the array, hence the [] around the {user attributes}.
  • it expects @account.users.build in the controller. I was not able to get the f.object.build_users syntax to work directly in the view.
Mark Berry
  • 17,843
  • 4
  • 58
  • 88
  • is "new.html.erb" a devise view (e.g. app/views/devise/registrations/new.html.erb)? – y0mbo Apr 25 '12 at 23:16
  • @y0mbo, no, new.html.erb is under views/accounts. I've added paths to the answer for clarification. See also the first paragraph about "bypassing the Devise view" and how that works. Basically I'm using the accounts#create action to create both an account and a user, so I don't need the Devise view. – Mark Berry Apr 26 '12 at 23:26
  • Great solution! This is exactly what I was looking for. One thing to note if an account has_one user, for example, it would be @account.build_user in the new action of the accounts controller. Also, the parts you mention at the end would be singular without the array syntax. – sq1020 Feb 11 '13 at 23:56
  • Awesome Answer! I've got it all working except for that when an account (and user) is created, it doen't seem to authenticate the user before redirecting. Any advice on how to put that behavior in? – Eric Mar 11 '13 at 21:46
  • Sorry @Eric, I've been away from this for awhile so it's hard to remember. I thought Devise sent an email requiring the user to confirm receipt before allowing them to log in. I remember having to do something to suppress that email when the admin creates additional users. – Mark Berry Mar 16 '13 at 18:41
1

Couldn't you use something like what's covered in these RailsCasts?:

http://railscasts.com/episodes/196-nested-model-form-part-1

http://railscasts.com/episodes/197-nested-model-form-part-2

You could setup your models as described in those screencasts, using accepts_nested_attributes_for.

Then, your views/devise/registrations/new.html.erb form would be for :user like normal, and could include a nested form for :account.

So something like this within that default form:

<%= f.fields_for :account do |account_form| %>
<div>
  <p>
    <%= account_form.label :name, "Account Name", :class => "label" %>
    <%= account_form.text_field :name, :class => "text_field" %>
    <span class="description">(e.g., enter your account name here)</span>
  </p>
</div>

<div>
  <p>
    <%= account_form.label :company, "Company Name", :class => "label" %>
    <%= account_form.text_field :company, :class => "text_field" %>
  </p>
</div>
<% end %>

This is sample code from an app I'm working on and I'm using the simple_form gem, so the helpers used in your app may be different, but you'll probably get the idea.

So when a user is created (when they register), they can also fill in the info that'll be used by the Account model to create their account once they hit the "Sign Up" button.

And you may want to set an attribute on that user like "admin" too...sounds like this initial user will be the "admin" user for the company, though other users may have access too.

Hope that helps.

Kenton Newby
  • 921
  • 5
  • 6
  • Thanks Kenton, very helpful. It looks like this will still require overriding the `create` action in the Devise registrations_controller in order to build the Account record. Cf. `@survey.questions.build` about 5 minutes into the first screencast. Or maybe the registration form should be for Account, but also call the Devise User `create` action. Re. an admin attribute, you're right, but I thought I'd better get this working before I worry about roles. – Mark Berry Nov 29 '11 at 19:42
  • Mark, glad that helped. It sounds like we're doing something similar. My app has similar functionality. In looking at my code again, I ended up having an account#new action and associated view where the signup form is. In that form, I create an account using form_for and within that form, have settings for :website and :user, similar to what's shown in the railscast. My guess is that when that nested user is created, it goes through devise and it's registration form, but I'm honestly not 100% sure on that. – Kenton Newby Nov 30 '11 at 03:28
  • So to clarify, you have `accepts_nested_attributes_for :user` in the Account model, and your account#new view calls `f.fields_for :user do |user_form|`? In other words, you're creating the account first, then the user is subordinate. I tried it the other way around without success--it seems accepts_nested_attributes_for must be on the _parent_ table. An account can say, "add me, then add some users to me"; a user can't say, "add a parent account before adding me." Relevant discussion in the Devise Google group: https://groups.google.com/d/topic/plataformatec-devise/7J3XB_RiHYs/discussion. – Mark Berry Nov 30 '11 at 19:49
  • Yes, that's correct. You may also need to add 'attr_accessor :user_attributes' in your Account model...so that user attributes can be set in the form. It's easy to forget the "_attributes" part since you don't normally have to have that. See the comments on the following page describing accepts_nested_attributes_for for more details: http://apidock.com/rails/ActiveRecord/NestedAttributes/ClassMethods/accepts_nested_attributes_for – Kenton Newby Dec 01 '11 at 17:39
  • thanks for pointing me to the nested attributes solution. I hope it's fair to mark that as "useful" and post a more complete example as an answer. – Mark Berry Dec 16 '11 at 03:20
0

The best solution would be to use a gem.

Easy way: milia gem

Subdomain way: apartment gem

Yshmarov
  • 3,450
  • 1
  • 22
  • 41