Pull to refresh

Полиморфные ассоциации и Devise в Ruby on Rails

Reading time7 min
Views4.7K
Всем привет.
Когда-то давно я писал статью про полиморфные ассоциации в Ruby on Rails и, помнится, некоторые негодовали: зачем, мол, писать про Rails 2, если на подходе новая версия.

Недавно мне пришлось столкнуться с полиморфными ассоциациями в Rails 3, а точнее придумать, как организовать на сайте два типа пользователей: заказчик и исполнитель. В этой статье речь пойдет о полиморфных ассоциациях и гемах Devise (для аутентификации) и CanCan (для авторизации).

Задача стояла следующая: есть два типа пользователей, регистрация и логин должны производиться из одной формы, при регистрации пользователь указывает, кем он хочет быть: заказчиком или исполнителем.
Соответственно, пользователи имели разные роли в проекте, могли делать разные вещи.

Таким образом, согласно стандартной документации Devise, я добавил его в проект. В миграцию, которую создал Devise, я добавил следующие поля:
  1. t.string :name, :null => false
  2. t.references :character, :polymorphic => true
* This source code was highlighted with Source Code Highlighter.

Кроме имени, сюда можно вынести любые другие общие свойства всех типов пользователей: страна, адрес и тд. У меня этим полем было только имя.
Вторая строка, согласно документации (http://apidock.com/rails/ActiveRecord/ConnectionAdapters/Table/references), создаст для нас два поля: character_id и character_type: в первом будут храниться id «персонажа», а во втором — название класса, где искать этот id.

После этого выполним rake db:migrate, а затем дополним модель user.rb.
  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
  5.  
  6. # Setup accessible (or protected) attributes for your model
  7. attr_accessible :name, :email, :password, :remember_me, :character_id, :character_type, :character, :character_attributes
  8.  
  9.  # Validations
  10.  validates_presence_of  :name, :character_type
  11.  validates_inclusion_of :character_type, :in => %w(Customer Executive)
  12.  
  13.  # Associations
  14.  belongs_to :character, :polymorphic => true, :dependent => :destroy
  15.  
  16.  # Nested attributes
  17.  accepts_nested_attributes_for :character
  18.  
  19.  # Authorization helper methods
  20.  def customer?
  21.     character_type == "Customer"
  22.  end
  23.  
  24.  def executive?
  25.     character_type == "Executive"
  26.  end
  27. end
* This source code was highlighted with Source Code Highlighter.

Здесь мы добавили новые поля в attr_accessible, добавили валидации, прописали ассоциации и добавили accepts_nested_attributes_for.
Изменения в attr_accessible нужны, чтобы можно было сохранять данные «пачками», а не по одной.
Валидации нужны, чтобы быть уверенными, что нужные нам записи добавлены (и их формат нас тоже устраивает).
Полиморфная ассоциация какбе намекает нам, что теперь у пользователя есть поле character, которое ссылается либо на заказчика, либо на исполнителя. По сути, это дополнение нашей модели User, которое может быть либо одного типа, либо другого.
assepts_nested_attributes_for нужны для того, чтобы можно было делать вложенные формы (форма для юзера, в которую вложена подформа для character’a (то есть либо для заказчика, либо для исполнителя).
Два метода в конце — просто для удобства, чтобы можно было легко и быстро определять в контроллерах и представлениям, какого типа у нас пользователь.

После этого создадим 2 модели: Customer и Executive. В миграциях можно прописать любые уникальные для каждого типа пользователя данные (пароли, явки и тд), в обоих моделях для связи с пользователем нужно прописать это:
  1. has_one :user, :as => :character, :dependent => :destroy
* This source code was highlighted with Source Code Highlighter.

Еще я добавил user_observer (rails g observer user), где написал такой код:
  1. class UserObserver < ActiveRecord::Observer
  2.  def before_create(user)
  3.     build_character_for user
  4.  end
  5.  
  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.

После этого в config/application.rb подключил этот обзервер:
  1. config.active_record.observers = :user_observer
* This source code was highlighted with Source Code Highlighter.

Помните, я говорил, что при регистрации пользователь должен выбрать, кем он хочет быть? У меня это radio, но вы легко можете сделать и select. В зависимости от выбранного типа, они должны возвращать либо “Customer”, либо “Executive”. И, соответственно, перед созданием каждого пользователя, мы создаем для него либо заказчика, либо исполнителя. Код выше можно записать в виде:

  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.

но это как-то долго и неудобно, согласитесь.

С Devise’ом покончено, теперь можно переходить к CanCan. Его тоже нужно установить согласно документации, а потом прописать разные правила для обоих типов пользователей. Например, как-то так:
  1. class Ability
  2.  include CanCan::Ability
  3.  
  4.  def initialize(user)
  5.     user ||= User.new
  6.     
  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
  16.         
  17.         # Collections
  18.         can :personal,                    Document
  19.         
  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        
  26.         
  27.         # Members
  28.         can :join,                        Document
  29.         can :leave,                      Document
  30.         
  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.

Допустим, как-то так. Таким образом, и у заказчиков, и у исполнителей есть доступ к одним и тем же контроллерам/ресурсам, но при этом каждому позволяются разные действия.

Теперь в представлении достаточно проверить, может ли текущий пользователь делать то или иное действия, и жизнь покажется Раем.
  1. if can? :join, @document
  2.  = link_to “Join”, [:join, @document]
* This source code was highlighted with Source Code Highlighter.

Теперь ссылку “Join” увидят только те, кому это разрешено в правилах выше. Нужно еще немного исправить контроллер, но там все согласно стандартной документации.

Подобным образом можно скрывать и показывать различные блоки на сайте, да и вообще что угодно.

Теперь еще пару слов об ассоциациях: большинство ассоциаций прописываются теперь в моделях заказчика и исполнителя, а не в модели пользователя. Таким образом достигается наибольшая гибкость при дальнейшей разработке.

Итог статьи таков: есть ситуации, когда полиморфные ассоциации действительно нужны и сильно упрощают жизнь. Не стоит фанатично добавлять их в каждую дырку, но стоит знать о них и уметь применить. 


Ссылки по теме:
github.com/plataformatec/devise
github.com/ryanb/cancan
habrahabr.ru/blogs/ror/79431
Tags:
Hubs:
Total votes 39: ↑36 and ↓3+33
Comments41

Articles