Consulting. Training. Development.

ActiveRecord inheritance and contexts

The User model appears almost in every Rails application and there are different types of users in our apps (admins and members at least). We often have different roles that are allowed to edit only specified fields or select only specified values.

In our application we have app admins and company (account) admins along with regular members. Company admins should be restricted to create only company admins and company members of their company. Regular members cannot change theirs role or company. App admins are allowed to do anything with users.

As both app admins and company admins have similar functionality it’d be good to have a one controller to manage users. We may have something like this in the admin/users controller:

1
2
3
4
5
6
7
class Admin::UsersController < ApplicationController
  def create
    @user = User.new(params[:user])
    @user.save
    respond_with :admin, @user
  end
end

But we need to restrict company admins from creating application admins and forbid changing their company. It means that we need to use different validations and different attr_accessible attributes. It can be achieved using attr_accessor in the User model or some complex parameter filtering in the controller, but let’s imaging how the controller code can be written to keep it simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Admin::UsersController < ApplicationController
  def create
    @user = user_base.new(params[:user])
    @user.company = current_user.company if current_user.company
    @user.save
    respond_with :admin, @user
  end

  private

  def user_base
    if current_user.company?
      User::CompanyAdminContext
    else
      User::AdminContext
    end
  end
end

In order to make the code above work it’s possible to use inheritance:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class User < ActiveRecord::Base
  belongs_to :company

  attr_accessible :email
  validates :role, inclusion: { in: %w(admin company_admin company_member) }

  class Context < User
    class << self
      def model_name
        User.model_name
      end
    end
  end

  class AdminContext < Context
    attr_accessible :role, :company_id
  end

  class CompanyAdminContext < Context
    attr_accessible :role
    validates :role, inclusion: { in: %w(company_admin company_member) }
  end
end

So we introduced User::AdminContext and User::CompanyAdminContext to be used in different cases. They have their own attr_accessible attributes and validations. Also we implemented the base User::Context to make contexts to use the same model_name as the User model.

This way allows the controller to choose a model behavior and the model isn’t got polluted with complex code.

Comments