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
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},
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: invoicexpress, RubyOnRails
