Popular Posts

четверг, 15 марта 2012 г.

Devise + Cancan. Часть три

Привет.
Продолжим с devise и cancan.
Что будем делать?
Допустим я хочу сделать так чтобы даже не вошедший пользователь мог бы сделать пост, нет
 он его не опубликует, просто его перекинет на страницу входа. Вот.
Для этого делаем контроллер таким:
#controllers/post_controller.rb
class PostsController < ApplicationController
 
  before_filter :authenticate_user!, :only => [:new]
  load_and_authorize_resource
  skip_authorize_resource :only => :new 
  #...
end
Как видно тут добавлен before_filter и метод :authenticate_user!, который только для действия new.
Метод :authenticate_user! он определён в devise.
Что он делает?
Если пользователь вошёл, то ничего (на самом деле он проверяет)
Если не произведён вход на сайт, то будет перенаправлен (redirect_to) на страницу входа /sign_in
Далее стандартная load_and_authorize_resource из cancan
Затем снова метод из cancan skip_authorize_resource :only => :new который сбрасывает проверку привилегий для
метода (:new).
Вот достаточно просто.
Ах да ещё убираем из view нашу проверку
#views/posts/index.html.erb
<%= link_to 'New Post', new_post_path %>
убрали can? перед ссылкой на создание нового поста.

Что дальше?
Для системы, которую делаю, неплохо бы добавить к постам комментарии, это же оживляет страницу)
Создаём модель\контроллер\и сразу миграцию
rails g model comment post:references user:references body:text
rails g controller comments
rake db:migrate
Хоп! Всё сделано, остался код + тесты
В класс модели User добавляем след. строчку:
#models/user.rb
class User < ActiveRecord::Base
# ...
has_many :comments, :dependent => :destroy
end
Этот макрос отношение один-ко-многим, то есть у одного пользователя может быть сотня-другая комментраиев.
В модель Post
#models/post.rb
class Post < ActiveRecord::Base
#...
has_many :comments, :dependent => :destroy
end
То же что и для User у одного поста много-много комментариев, с удалением поста все они тоже будут удалены.
Ну и далее нужная нам новая модель Comment
#models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :post
  belongs_to :user
  validates :user_id, :post_id, :body, :presence => true
  attr_accessible :body
end
Первые методы belongs_to указываеют на связь к таблицам posts\users.
С validates понятно у комментария должен быть пользователь\пост\ну и собственно сам текст комментария.
attr_accessible -- (та строчка, из-за которой была паника на GitHub) указывает на то, что можно изменять только это поле (:body), но
не пользователя и пост.
Затем добавляем роутинг.
#/config/routes.rb
resources :posts do
  resources :comments, :only => [:create, :destroy]
end
Тут вложенный ресурс comments ссылки будут выглядеть posts/post_id/comments/
То есть навигация по комментариям идёт уже в определённом посте.
Вот часть сделали. Теперь контроллер
#controllers/comments_controller.rb
class CommentsController < ApplicationController
  load_and_authorize_resource
  def create
    @post = Post.find(params[:post_id])
    comment = @post.comments.build(params[:comment])
    comment.user = current_user

    if comment.save
      flash[:notice] = 'Comment was successfully created.'
      redirect_to post_path(@post)
    else
      flash[:notice] = 'The comment you typed was invalid.'
      render "posts/show"
    end
  end

  def destroy
    post = Post.find([params[:post_id]])
    comment = Comment.find(params[:id])
    comment.delete
    flash[:notice] = "Delete"
    redirect_to post_path(post)
  end

  rescue_from CanCan::AccessDenied do |exception|
    flash[:notice] = "You can not create or delete comment"
    redirect_to post_path(Post.find(params[:post_id]))
  end
end
В контроллере есть методы destroy, который удаляет комментарий и метод create, который создаёт комментарий.
Также видна строчка из cancan load_and_authorize_resource, которую я добавил (а значит изменил и models/ability.rb)
Потом важная вещь переопределил исключение, когда действие не допустимо. Если помните такое же переопределение есть в
ApplicationController так вот там это исключение, если пользователь делает что-то в не своих правах, то будет переброшен
на root_url, а здесь просто возврашаем на тот же пост. Вот и всё.
Методы также просты и не должны вызывать непонимания (чуть что в комментариях задайте вопрос)

Так теперь Ability
#models/ability.rb
class Ability
#...
 def initialize(user)
   #...
   if user.role? :admin
     #...
     can [:create, :destroy], Comment
   else
    if user.role?(:user)
       can :create, Comment
    #...
   end
 end
end
Показал только то, что добавил. Обычный пользователь может комментировать, а админ плюс ко всему и удалять неугодные комментарии)))
Теперь с view.
Форма для создания комментария
#views/comments/_form.html.erb
<% if can? :create, Comment %>
  <%= form_for [@post, current_user.comments.new] do |f| %>
    <%= f.text_area :body, :size => "70%x10" %>
    <br/>
    <%= f.submit "add comment" %>
  <% end %>
<% end %> 
Снова метод can? который проверяет может ли пользователь создавать комментарий. Ну и потом стандартная форма.
Затем для вывода комментариев для поста:
#views/comments/_index.html.erb
<% @post.comments.each do |comment| %>
  <p>
    <b>
      Name:<%= comment.user.email %>
    </b>
    <%= comment.body %>
    <% if can? :clear, Vote %>
      <%= link_to 'delete', post_comment_path(@post, comment), method: :delete %>
    <% end %>
  </p> 
<% end %>
Тут просто выводим email пользователья (потом улучшим будет имя) ну и сам комментарий. А также ссылка для админа, который может удалить комментарий. Формирование ссылки посмотрите, как строится: если мы наберём
rake routes
то покажется куча роутов среди которых есть и post_comment_path, для которого нужен пост и коммент, что мы и передаём ему, а rails сам формирует правильную ссылку.
Теперь добавляем наши view в вывод поста:
#views/posts/show.html.erb
<%= render 'comments/index'%>
<%= render 'comments/form' %>
Просто добавте пару строчек и всё (после вывода поста конечно же, но можете и перед, как угодно).

Ну и теперь куча тестов:
Сначала для PostsController, там же добавили некоторые вещи.
#spec/controllers/post_controller_spec.rb
  describe "GET new" do
    before(:each) {
      @user = Factory(:user)
      @user.roles << Factory(:role) 
      sign_in @user
    }
    it "asigns a new post as @post" do
      get 'new'
      assigns(:post).should be_a_new(Post)
      response.should render_template('posts/new')
    end
    it "asigns a new post as @post if User auth" do
      get 'new'
      assigns(:post).should be_a_new(Post)
      response.should render_template('posts/new')
    end
    it "should redirect_to if user not auth" do
      sign_out @user
      get 'new'
      response.should redirect_to(new_user_session_path)
    end
  end
Надеюсь названия понятны и говорят сами за себя.
Ну и для CommentsController
/spec/controllers/comments_controller_spec.rb
require 'spec_helper'

describe CommentsController do
  describe "POST create" do
    before(:each) {
      @user = Factory(:user)
      @user.roles << Factory(:role)
      @post = Factory(:post, :user => @user)
    }
    it "should create new comment if user can create comments" do
      sign_in @user
      attr = {:body => "new comment"}
      post 'create', {:post_id => @post.id, :comment => attr }
      flash[:notice].should eq('Comment was successfully created.')
      response.should redirect_to(post_path(@post))  
    end
    it "should render posts/show if attr is invalid" do
      sign_in @user
      post 'create', {:post_id => @post.id, :comment => {}}
      flash[:notice].should eq('The comment you typed was invalid.')
      response.should render_template('posts/show')  
    end
    it "should not create new comment if user can not ability" do
      post 'create', {:post_id => @post.id, :comment => {}}
      flash[:notice].should eq("You can not create or delete comment")
      response.should redirect_to(post_path(@post))
    end
  end

  describe "DELETE destroy" do
    before(:each) {
      @post = Factory(:post, :user => Factory(:user))
      comment = Factory(:comment, :user => Factory(:user), :post => @post)
    }
    it "should delete comment if admin" do
      user = Factory(:user)
      user.roles << Factory(:role, :name => :admin)
     
      sign_in user
      
      @post.comments.count.should eq(1)
     
      delete 'destroy', { :post_id => @post.id, :id => @post.comments.last.id }
     
      flash[:notice].should eq("Delete")
      response.should redirect_to(post_path(@post))
      @post.comments.count.should eq(0)
    end
    it "should not delete comment if user" do
      user = Factory(:user)
      user.roles << Factory(:role)
     
      sign_in user
      
      @post.comments.count.should eq(1)
     
      delete 'destroy', { :post_id => @post.id, :id => @post.comments.last.id }
     
      flash[:notice].should eq("You can not create or delete comment")
      response.should redirect_to(post_path(@post))
      @post.comments.count.should eq(1)
    end
  end
end

1 комментарий:

  1. Спасибо за сбор инфы в одном мете =) давно искал
    P.S.
    skip_authorize_resource :only => :new ( можно вроде как убрать - я чет не увил что он даёт - так как, если ты авторизован ты в любом случае можешь создавать посты, а если нет - до skip-a управление не дойдет ( так как devise перехватит и отредеректит на страницу с логином )

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