Polymorphic Associations and Devise in Ruby on Rails

    Once upon a time I wrote an article about polymorphic associations in Ruby on Rails and, I remember, some were indignant: why, they say, write about Rails 2, if the new version is coming.

    Recently, I had to deal with polymorphic associations in Rails 3, or rather come up with how to organize two types of users on a site: a customer and an executor. This article will focus on polymorphic associations and gems Devise (for authentication) and CanCan (for authorization).

    The task was as follows: there are two types of users, registration and login must be made from one form, during registration the user indicates who he wants to be: the customer or the contractor.
    Accordingly, users had different roles in the project, they could do different things.

    Thus, according to the standard Devise documentation, I added it to the project. In the migration that Devise created, I added the following fields:
    1. t.string :name, :null => false
    2. t.references :character, :polymorphic => true
    * This source code was highlighted with Source Code Highlighter.

    In addition to the name, here you can make any other common properties of all types of users: country, address, etc. I had only a name in this field.
    The second line, according to the documentation (http://apidock.com/rails/ActiveRecord/ConnectionAdapters/Table/references), will create two fields for us: character_id and character_type: in the first, the id of the “character” will be stored, and in the second - the name of the class where to look for this id.

    After that, run rake db: migrate, and then add the user.rb model.
    1. class User < ActiveRecord::Base
    2.  # Include default devise modules. Others available are:
    3.  # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
    4.  devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable
    6. # Setup accessible (or protected) attributes for your model
    7. attr_accessible :name, :email, :password, :remember_me, :character_id, :character_type, :character, :character_attributes
    9.  # Validations
    10.  validates_presence_of  :name, :character_type
    11.  validates_inclusion_of :character_type, :in => %w(Customer Executive)
    13.  # Associations
    14.  belongs_to :character, :polymorphic => true, :dependent => :destroy
    16.  # Nested attributes
    17.  accepts_nested_attributes_for :character
    19.  # Authorization helper methods
    20.  def customer?
    21.     character_type == "Customer"
    22.  end
    24.  def executive?
    25.     character_type == "Executive"
    26.  end
    27. end
    * This source code was highlighted with Source Code Highlighter.

    Here we added new fields to attr_accessible, added validations, registered associations and added accepts_nested_attributes_for.
    Changes to attr_accessible are needed so that you can save data in batches, rather than one at a time.
    Validations are needed to make sure that the records we need are added (and their format also suits us).
    The Kakbe polymorphic association hints to us that the user now has a character field that refers to either the customer or the artist. In fact, this is an addition to our User model, which can be either of one type or of another.
    assepts_nested_attributes_for are needed so that you can make nested forms (a form for the user, in which a subform for character is embedded (that is, either for the customer or for the performer).
    The two methods at the end are just for convenience, so that you can easily and quickly determine in controllers and views what type of user we have.

    After that we will create 2 models: Customer and Executive. In migrations, you can register any data unique to each type of user (passwords, attendance, etc.), in both models, to communicate with the user, you need to register this:
    1. has_one :user, :as => :character, :dependent => :destroy
    * This source code was highlighted with Source Code Highlighter.

    I also added user_observer (rails g observer user), where I wrote this code:
    1. class UserObserver < ActiveRecord::Observer
    2.  def before_create(user)
    3.     build_character_for user
    4.  end
    6.  private
    7.  def build_character_for(user)
    8.     user.character = user.character_type.classify.constantize.create!
    9.  end
    10. end
    * This source code was highlighted with Source Code Highlighter.

    After that, I connected this observer in config / application.rb:
    1. config.active_record.observers = :user_observer
    * This source code was highlighted with Source Code Highlighter.

    Remember, I said that when registering, the user must choose who he wants to be? I have it radio, but you can easily make select. Depending on the type selected, they should return either “Customer” or “Executive”. And, accordingly, before creating each user, we create either a customer or an executor for him. The code above can be written as:

    1. if user.character_type == “Customer”
    2.  user.character = Customer.create!
    3. else
    4.  user.character = Executive.create!
    5. end
    * This source code was highlighted with Source Code Highlighter.

    but it’s somehow long and uncomfortable, agree.

    With Devise'om over, now you can go to CanCan. It also needs to be installed according to the documentation, and then different rules should be set for both types of users. For example, something like this:
    1. class Ability
    2.  include CanCan::Ability
    4.  def initialize(user)
    5.     user ||= User.new
    7.     if user.admin?        # Admin account
    8.      can :manage, :all
    9.     else
    10.      if user.customer?  # Customer account
    11.         # RESTful
    12.         can :read,                        Document
    13.         can :create,                     Document
    14.         can :update,                     Document,    :customer_id => user.character.id
    15.         can :read,                        Comment
    17.         # Collections
    18.         can :personal,                    Document
    20.      elsif user.executive? # Executive account
    21.         # RESTful
    22.         can :read,                        Document
    23.         can :read,                        Comment
    24.         can :create,                     Comment
    25.         can [:update, :destroy],         Comment, :executive_id => user.character.id        
    27.         # Members
    28.         can :join,                        Document
    29.         can :leave,                      Document
    31.         # Collections
    32.         can :drafts,                     Comment
    33.         can :archive,                     Comment
    34.      end
    35.     end
    36.  end
    37. end
    * This source code was highlighted with Source Code Highlighter.

    Let's say something like that. Thus, both customers and performers have access to the same controllers / resources, but at the same time, each person is allowed different actions.

    Now in the view it is enough to check whether the current user can do one or another action, and life will seem like Paradise.
    1. if can? :join, @document
    2.  = link_to “Join”, [:join, @document]
    * This source code was highlighted with Source Code Highlighter.

    Now the “Join” link will be visible only to those who are allowed in the rules above. You need to fix the controller a little more, but everything is there according to the standard documentation.

    Similarly, you can hide and show various blocks on the site, and indeed anything.

    Now a few words about associations: most associations are now registered in the models of the customer and the executor, and not in the user model. Thus, the greatest flexibility is achieved with further development.

    The result of the article is as follows: there are situations when polymorphic associations are really needed and greatly simplify life. You should not fanatically add them to each hole, but you should know about them and be able to apply them. 

    Related links:

    Also popular now: