Rupeal
RSSLinkedInTwitterFacebook

One user – Multiple accounts

One of the new features we have for the next release of InvoicExpress is to allow a user to be a member of multiple accounts.

The advantage of this model is that you only need one login credential to use all the accounts that you have access to.

In this post we show you a glimpse of our development process while developing this feature.

Our starting data model was (simplified here for clarity), a user belongs to an account, an account has many users, a user has many roles, nothing fancy.

class Account < ActiveRecord::Base
  has_many :users
end

class User < ActiveRecord::Base
  belongs_to :account
  has_and_belongs_to_many :roles
end

class Roles < ActiveRecord
  has_and_belongs_to_many :users
end

Users, Accounts and Memberships

The most obvious change is that the relation between user and account evolves from a one-to-many to many-to-many.

This change means user roles now have to be scoped by account. This isn’t easy with a regular many-to-many association.

In plain english we say “a user is a member of an account”. Member? We don’t have that in our data model. The relation between a user and its account is called Membership. Naming this showed us the missing link between a user and its accounts.

So we add a Membership model to map the relation between a user and it’s account.

User roles are now associated to an account through a membership.

class Account < ActiveRecord::Base
  belongs_to :o wner, :class_name => "User",
                     :foreign_key => "owner_id" 

  has_many :memberships, :dependent => :destroy
  has_many :users,:through => :memberships

end

class User<; ActiveRecord::Base
  has_many :memberships
  has_many :accounts, :through => :memberships,
                      :uniq => true
end

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :account

  has_and_belongs_to_many :roles
end

class Roles < ActiveRecord::Base
end

Keeping track

With multiple accounts per user we need know the currently used account. Our choice was to do it at the database level, because it would imply the least changes to our code base.

Knowing the current account means we also know the current membership. User roles can now be scoped to the current membership.

class User < ActiveRecord::Base

  has_many :memberships
  has_many :accounts, :through => :memberships,
                                   :uniq => true

  belongs_to :current_account, :class_name => "Account"
  has_one :current_membership,
          :class_name => "Membership",
          :foreign_key => "user_id",
          :conditions => 'current_account_id = #{self.current_account.id}'

  def roles
    self.current_membership.roles unless current_membership.blank?
  end
end

And that’s it for the data model changes

Changing accounts

Having multiple accounts, the user needs to know which one he is currently working on, as well as, be able to easily select another account.

For this double duty, we chose to use a always visible combo box located on the header of InvoicExpress.

The following code samples are simplified for readability.

Here we create a select input on our header partial and wire the onChange event to an ajax call to the users_controller.

# _header.html.erb
...
<%if current_user.accounts.count > 1 -%>;
  <%= select "change",:account, current_user.accounts.map{|a| [a.name, a.id]},
                                {:selected => current_user.account.id},
                                :o nchange => change_account
  %>
<% end -%>
...
# application_helper.rb
...
def change_account
  remote_function(:url => change_current_account_path, :method => :post,
                  :with =>"$('change_account').serialize()"
                 )
end

The change_current_account action fetches the selected account from the accounts the user is a member of.

If none is found the save will fail and we return a 412 – Precondition Failed error. On a successful save the user is now scoped the selected account, and we just have to redirect to the selected account account home screen.

# user_controller.rb
...
def change_current_account
  current_user.current_account = current_user.accounts.find(params[:change][:account])

  respond_to do |format|
    if current_user.save
      flash[:notice] = "You are now using account: #{account.name}"
      format.js do
        render :update do |page|
           page.redirect_to home_url
        end
      end
    else
        format.js{render :text => "change account error", :status =>; 412}
    end
  end
end

Wrapping up

Some say that every programming problem can be solved by another layer of indirection.
Others say that naming things is the hardest part of programming, and in this case both were true.

After we acknowledged and named the relation between a user and its accounts as a membership, all issues found almost resolved themselves. We were following a clear path. We were on rails after all.

Ps: In case you’re wondering the plugins we’re using for authentication and authorization are:

both available on github

Tags: ,

Bruno Coelho on July 3rd, 2009

Software Development

0
 

Deixe um Comentário