Popular Posts

понедельник, 5 марта 2012 г.

Devise + Cancan -- быстрая разработка


Привет.
Сегодня посмотрим как работать с devise и cancan. Важные gem'ы, да?
и так сперва задача:

  •   Авторизация\регистрация\аутентификация пользователя на сайте -- это задача devise
  •   Потом привилегии: 
  •   админ может всё ;)
  • пользователь может создавать посты
  • автор постов (владелец) может его редактировать, но удалять не может.

Не очень сложно.
и так в Gemfile добавляем
gem 'devise'
gem 'cancan'

Ok, установили теперь devise:
rails g devise:install
Эта команда создаст файл инициализации, нам нужно его отредактировать:
 config/initializers/devise.rb
находим строчку
 config.sign_out_via = :delete
меняем на :get.
Так хорошо. Теперь делаем модель пользователя:
rails g devise user
Создаёт модель. Ну и сразу делаем миграцию, что создаёт таблицу в БД:
rake db:migrate
импортируем views
rails g devise:views
Ок. Теперь делаем для постов, тут легче:
rails g scaffold Post title:string content:text user:references
rake db:migrate
Всё! Что хотели, то сделали: можно регистрироваться и все дела.
Теперь время добавить привилегии с cancan:
rails g cancan:ability
Создаёт класс возможностей\привилегий в app/models.
Ну в принципе каркас написан, осталось добавить немного кода.
Но перед этим нужны же ещё Роли (Role). У пользователя может быть тысяча ролей, и модератор и просто_пользователь в общем придумайте сами.
У меня -- это будет admin\user и всё. и так как видно это отношение has_many:through


rails g model role name:string
rails g model users_role user:references role:references


Всё теперь редактируем модель User, добавляем зависимости:
class User < ActiveRecord::Base
  has_many :users_roles
  has_many :roles, :through => :users_roles
end

Модель Role
class Role < ActiveRecord::Base
  has_many :users_roles
  has_many :users, :through => :users_roles
end


Всего лишь строчку в PostController:
load_and_authorize_resource
и всё.
Теперь с представлением:
Ну допустим раньше выводилось вот так:
#views/index.html.erb
<% @posts.each do |post| %>
  <tr>
    <td><%= post.title %></td>
    <td><%= post.content %></td>
    <td><%= post.user %></td>
    <td><%= link_to 'Show', post %></td>
    <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td>
  </tr>
<% end %>
<br />
<%= link_to 'New Post', new_post_path %>
Теперь с помощью волшебного метода can делаем:
<h1>Listing posts</h1>


<table>
  <tr>
    <th>Title</th>
    <th>Content</th>
    <th>User</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>


<% @posts.each do |post| %>
  <tr>
    <td><%= post.title %></td>
    <td><%= post.content %></td>
    <td><%= post.user %></td>
    <% if can? :read, post %>
      <td><%= link_to 'Show', post %></td>
    <% end %>
    <% if can? :update, post %>
      <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <% end %>
    <% if can? :destroy, post %>
      <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td>
    <% end %>
  </tr>
<% end %>
</table>


<% if can? :create, Post %>
  <%= link_to 'New Post', new_post_path %>
<% end %>
Также не забывайте в контроллере PostsController в методе create устанавливать связь пользователя  и нового поста:
def create
    @post = current_user.posts.new(params[:post])
  ....
end

Потом напишем немного больше и продолжим с cancan + добавим спеки (rspec)
Немного UPD
Чтобы сделать меню, где вход\выход -- стандратное меню, нужно например в views/layouts/application.html.erb
добавить что-то вроде этого:

<div id="header">
  <%= render "shared/links" %>
</div>
Папку shared создайте сами, и в ней файл _links.html.erb

<ul>
  <% if user_signed_in? %>
    <li><%= link_to "My Profile", edit_user_registration_path%> </li>
    <li><%= link_to "Sign out", destroy_user_session_path%> </li>
  <% else %>
    <li><%= link_to "Sign up", new_user_registration_path%> </li>
    <li><%= link_to "Sign in", new_user_session_path%> </li>
  <% end %>
</ul>   

Потом для правильно отрисовки в app/helpers/application_helper.rb:

module ApplicationHelper
  def resource_name
    :user
  end


  def resource
    @resource ||= User.new
  end


  def devise_mapping
    @devise_mapping ||= Devise.mappings[:user]
  end
end

Ok, всё круто, но я чувствую у вас вопрос: Откуда взять роль, когда пользователь зарегистрировался? Ответ -- callbacks
В модели User:
#models/user.rb
class User < ActiveRecord::Base
   before_create :create_role
  
  has_many :posts #надеюсь вы не забыли установить эту зависимость :)
  private
    def create_role
      self.roles << Role.find_by_name(:user)  
    end
end
Ещё UPD
Когда мы ищём роль: Role.find_by_name(:user), то предпологается, что уже есть такая роль. Поэтому перед запуском приложения в файле db/seeds.rb
Role.create(:name => :admin)
Role.create(:name => :user)
И потом в консоли rake db:seed


UPD код: http://code.google.com/p/example-app/


Прошлый пост

19 комментариев:

  1. в статье есть не совпадения. делал всё как в ней в результате в User.rb:

    class User < ActiveRecord::Base
    # Include default devise modules. Others available are:
    # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable

    devise :database_authenticatable, :registerable,
    :recoverable, :rememberable, :trackable, :validatable

    # Setup accessible (or protected) attributes for your model
    attr_accessible :email, :password, :password_confirmation, :remember_me
    before_create :create_role
    has_many :users_roles
    has_many :roles, :through => :users_roles
    has_many :posts
    private
    def create_role
    self.roles << Role.find_by_name(:user)
    end
    end

    как видно много строчек отсутствуют.
    и еще в статье написано:
    "Всего лишь строчку:
    load_and_authorize_resource"
    куда её добавлять совершенно не понятно.

    ОтветитьУдалить
    Ответы
    1. строчку в PostController там вроде было написано, но пропало! удивительно в общем.
      Я думаю лучше заливать такие вещи на гит. Что и сделаю вечером.
      А что не понятно с моделью User? То что там много кода? так это генерирует devise, а я привёл только то, что нужно добавить.

      Удалить
    2. Этот комментарий был удален автором.

      Удалить
    3. Совершено не понятно что приведенный код нужно ДОБАВИТЬ - ведь там присутствует class User < ActiveRecord::Base, но отсутствуют комментарии-метки где находится уже сгенерённый код.
      Впрочем, если планируется заливать код на github такая необходимость пропадёт сама собой.

      Удалить
    4. я залил на гугл коде. Там есть в конце. Остально учту.

      Удалить
  2. Ответы
    1. вы должны установить http://mercurial.selenic.com/ меркуриал и сделать hg clone url_adress

      Удалить
  3. А не подскажите какие версии используются в данном примере?

    ОтветитьУдалить
  4. Спасибо, уже скачал, посмотрел.

    ОтветитьУдалить
  5. А где в статье написано как привязывать роли к юзерам???

    ОтветитьУдалить
  6. Этот комментарий был удален автором.

    ОтветитьУдалить
    Ответы
    1. Этот комментарий был удален автором.

      Удалить
    2. Этот комментарий был удален автором.

      Удалить
  7. Подскажите, использовала devise (без кан-кана) и создала scaffold post.

    Во первых, не работает код:
    @post = current_user.posts.new(params[:post])

    Выходит ошибка: NoMethodError in PostsController#create
    undefined method `posts' for #

    Пришлось сделать
    @post = Post.new(params[:post])
    @post.user = current_user
    Почему?

    Во вторых, как правильно ограничить действия Edit и Destroy для не авторов?

    То есть я сделаю в views/posts/index.html.erb

    <% if post.user == current_user %>
    Edit/Destroy
    <% else %>
    <% end %>

    А как правильно запретить в контроллере доступ к этим действиям?

    ОтветитьУдалить
    Ответы
    1. Ответ только на второй вопрос, средствами devise не осуществляется ограничение действий по ролям, devise это средство авторизации, не совсем правилен сам подход использования devise для этого, советую все таки посмотреть в сторону cancan. В скринкасте rayanb по этому поводу все отлично пояснено: http://railscasts.com/episodes/192-authorization-with-cancan. Например для вашего случая с Edit и Destroy код будет следующим:
      if user.role?(:author)
      can :create, Post
      can :update, Post do |p|
      p.try(:user) == user
      end
      can :destroy, Post do |p|
      p.try(:user) == user
      end
      end

      Удалить
  8. По статье непонятен смысл замены config.sign_out_via = :delete
    меняем на :get.

    По своему принципу убийство сессии должно происходить методом :delete и данный метод отлично работает. Для чего меняется на :get?

    ОтветитьУдалить
  9. Так же небольшой UPD:
    "Папку shared создайте сами, и в ней файл _links.html.erb" Данные действия не нужны как минимум в версии devise 2.2.3 при генерации вьюх девайса сейчас оно само генерится

    ОтветитьУдалить