Привет.
Продолжаем, обещанное вчера -- cancan + devise.
и так создадим ещё одну модель для того, чтобы пользователь мог бы голосовать, ну то есть обычный рейтинг поста.
ли пользователь или нет, если что это отношение has_many
а потом говорю, что обращение к методу clear следует через delete метод. Можно было бы записать :only => [:update, :delete], но я хотел именно метод clear, который отражает суть.
Так теперь редактирую модели:
Затем добавил привилегию для админа, что он может обнулять голосование.
и добавил проверку на новую запись -- это для гостей, то есть тех, кто не вошёл в систему.
Ну и дальше стандартная штука проверил голосовал ли пользователь .include?(user).
Вот и всё.
Теперь обещанные тесты (только для контроллеров, модели легко проверяются), я не буду давать пояснения к каждому методу, я старался описывать всё,
если что спросите в комментариях. Тесты достаточно понятны, единственное, что может быть не понятно -- это мой английский, которым я владею не очень хорошо ;)
Дальше TL; DR
Сначала фабрики:
Вот и всё. Все 24 теста зелёные.
UPD: код на гугле: http://code.google.com/p/example-app/
Продолжаем, обещанное вчера -- cancan + devise.
и так создадим ещё одну модель для того, чтобы пользователь мог бы голосовать, ну то есть обычный рейтинг поста.
rails g model vote score:integerЗатем я хочу, чтобы пользователь мог бы только один раз голосовать, поэтому создаю таблицу, где будет показано голосовал
ли пользователь или нет, если что это отношение has_many
# rails g model users_vote user:references vote:references #Остался контроллер для голосования:
rails g controller voteОк, теперь делаем роутинг: открываем config/routes.rb и записываем:
resources :posts do resources :vote, :only => [:update] do delete 'clear' end endЧто написали? Получается, что ресурс Post содержит в себе ресурс Vote у которого я оставил один стандартный глагол (:update),
а потом говорю, что обращение к методу clear следует через delete метод. Можно было бы записать :only => [:update, :delete], но я хотел именно метод clear, который отражает суть.
Так теперь редактирую модели:
#user.rb class User < ActiveRecord::Base #......... has_many :users_votes has_many :votes, :through => :users_votes end #vote.rb class Vote < ActiveRecord::Base belongs_to :post has_many :users_votes has_many :users, :through => :users_votes end #users_vote.rb class UsersVote < ActiveRecord::Base belongs_to :user belongs_to :vote endи да обновим Post, ведь нужно же создавать как-то голосование, при создании поста:
class Post < ActiveRecord::Base after_create :create_vote #кэлбэк! has_one :vote, :dependent => :destroy #не забываем про отношения! protected def create_vote self.vote = Vote.create(:score => 0) end endВот всё, как видно связь пользователь и голосов через промежуточную таблицу users_votes Теперь напишем контроллер для голосования:
class VoteController < ApplicationController #сразу загружаем ресурсы cancan load_and_authorize_resource #а вот и метод update, который увеличивает счётчик голосования def update post = Post.find(params[:post_id]) vote = post.vote vote.score += 1 if params[:vote][:score] == "Up" vote.score -= 1 if params[:vote][:score] == "Down" vote.users << current_user vote.save flash[:notice] = "You vote counted!" redirect_to post_path(post) end #а этот метод позволяет админу обнулять голосование за пост, #читерство, махинации и подтасовка) def clear post = Post.find(params[:post_id]) post.vote.update_attributes(:score => 0) flash[:notice] = "Clear!" redirect_to post_path(post) end #также я переопределил исключение, помните в прошлом посте было? #Так вот если оставить стандартное, то #будет перенаправлять на главную, а мне нужно, чтобы оставалось на этой же странице. rescue_from CanCan::AccessDenied do |exception| flash[:notice] = "You have already voted" redirect_to post_path(Post.find(params[:post_id])) end endЗаметили такую плохую вещь как vote.users << current_user -- я думаю плохой стиль, просто current_user нельзя использовать в моделях, а так бы лучше написать callback. Теперь шаблоны обновим: Простой до невозможности шаблон
#views/vote/_update.html.erb <% if can? :update, Vote %> <%= form_for [@post, @post.vote] do |f| %> <%= f.label :score, "+"%> <%= f.radio_button :score, "Up" %> <%= f.label :score, "-"%> <%= f.radio_button :score, "Down" %> <%= f.submit "vote" %> <% end %> <% end %> <% if can? :clear, Vote %> <%= link_to 'clear', post_vote_clear_path(@post, @post.vote), method: :delete %> <% end %>Так этот шаблон рендерится из views/posts/show.html.erb
<%= render 'vote/update' %>Всё с этим закончили отредактируем Ability (наш cancan)
#models/ability.rb #Станет такой уже метод def initialize(user) user ||= User.new if user.role? :admin can :manage, Post can :clear, Vote else can :read, :all if user.role?(:user) can :create, Post can :update, Post do |post| post.try(:user) == user end end end unless user.new_record? can :update, Vote do |vote| !vote.users.include?(user) end endКто заметил, что теперь админ управляет только Posts? Если бы я оставил :all, то админ мог бы голосовать до бесконечности. Подумайте почему.
Затем добавил привилегию для админа, что он может обнулять голосование.
и добавил проверку на новую запись -- это для гостей, то есть тех, кто не вошёл в систему.
Ну и дальше стандартная штука проверил голосовал ли пользователь .include?(user).
Вот и всё.
Теперь обещанные тесты (только для контроллеров, модели легко проверяются), я не буду давать пояснения к каждому методу, я старался описывать всё,
если что спросите в комментариях. Тесты достаточно понятны, единственное, что может быть не понятно -- это мой английский, которым я владею не очень хорошо ;)
Дальше TL; DR
Сначала фабрики:
#spec/factories.rb FactoryGirl.define do factory :post do sequence(:title) {|n| "New Title -- #{n}" } content "MyText" user end end #/roles.rb FactoryGirl.define do factory :role do name "user" end end #/users.rb FactoryGirl.define do factory :user do #здесь генерирую email'ы по возрастанию, потому что пользуюсь #sqlite, для которой Database_cleaner Не действует. sequence(:email) {|n| "email#{n}@abc.com" } password "1234567" end end #votes.rb FactoryGirl.define do factory :vote do score 0 end end #users_votes.rb - пустая фабрика FactoryGirl.define do factory :users_vote do end endСначала для контроллера Post:
require 'spec_helper' describe PostsController do describe "GET index" do it "assign all posts" do post = Factory(:post) get 'index' assigns(:posts).should eq([post]) response.should render_template('posts/index') end end describe "GET show" do it "should show post" do post = Factory(:post) get 'show', {:id => post.id} assigns(:post).should eq(post) response.should render_template('posts/show') end end describe "GET new" do before(:each) { @user = Factory(:user) @user.roles << Factory(:role) sign_in @user } it "ssigns a new post as @post if User have ability :user" 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 have ability :admin" do @user.roles.clear @user.roles << Factory(:role, :name => :admin) get 'new' assigns(:post).should be_a_new(Post) response.should render_template('posts/new') end it "ssigns a new post as @post if User have not ability :user" do sign_out @user get 'new' response.should redirect_to(root_url) end end describe "GET edit" do before(:each) { @user = Factory(:user) @user.roles << Factory(:role) @post = Factory(:post, :user => @user) } it "can edit post if i'm author this post" do sign_in @user get 'edit', {:id => @post.id } assigns(:post).should eq(@post) response.should render_template('posts/edit') end it "can edit post if i'm the admin" do @user.roles.clear @user.roles << Factory(:role, :name => :admin) sign_in @user get 'edit', {:id => @post.id} assigns(:post).should eq(@post) response.should render_template('posts/edit') end it "can not edit post if not author" do user = Factory(:user) sign_in user get 'edit', {:id => @post.id} response.should redirect_to(root_url) end it "can not edit post if not authorized" do get 'edit', {:id => @post.id} response.should redirect_to(root_url) end end describe "POST create" do context "should create post if params is valid" do before(:each) { @attr = { :title => "new title.", :content => "new content." } } it "should create post if user can create post" do user = Factory(:user) user.roles << Factory(:role) sign_in user post 'create', {:post => @attr} response.should redirect_to(post_path(Post.last)) assigns(:post).should be_a(Post) assigns(:post).should be_persisted end it "redirect_to root_url if user can not create post" do post 'create', {:post => @attr} response.should redirect_to(root_url) end end context "should not create post if params is not valid" do it "should re-render new template if params is invalid" do attr = {:title => "new title"} user = Factory(:user) user.roles << Factory(:role) sign_in user post 'create', {:post => @attr} response.should render_template('posts/new') end end end describe "PUT update" do before(:each) { @user = Factory(:user) @user.roles << Factory(:role) @post = Factory(:post, :user => @user) } it "should update post if it is my post" do sign_in @user put 'update', { :id => @post.id, :post => {:title => 'update title'}} response.should redirect_to(post_path(@post)) end it "should update post if i'm the admin" do @user.roles.clear @user.roles << Factory(:role, :name => :admin) sign_in @user put 'update', { :id => @post.id, :post => {:title => 'update title'}} response.should redirect_to(post_path(@post)) end it "should not update post if it is not my post" do user = Factory(:user) user.roles << Factory(:role) sign_in user put 'update', { :id => @post.id, :post => {:title => 'update title'}} response.should redirect_to(root_url) end it "should not update post and redirect_to root_url if i can not ability" do put 'update', { :id => @post.id, :post => {:title => 'update title'}} response.should redirect_to(root_url) end end describe "DELETE destroy" do before(:each) { @user = Factory(:user) @user.roles << Factory(:role) @post = Factory(:post, :user => @user) } it "should delete post if i am the admin" do @user.roles.clear @user.roles << Factory(:role, :name => :admin) sign_in @user delete 'destroy', { :id => @post.id } flash[:notice].should eq("Delete") response.should redirect_to(posts_path) end it "should not delete post if i am author" do sign_in @user delete 'destroy', { :id => @post.id } response.should redirect_to(root_url) end it "should not delete post if i am guest" do delete 'destroy', { :id => @post.id } response.should redirect_to(root_url) end end endи вот для Vote
require 'spec_helper' describe VoteController do describe "PUT update" do before(:each) { @user = Factory(:user) @user.roles << Factory(:role) @post = Factory(:post, :user => @user) } it "should vote if not voted" do sign_in @user put 'update', {:post_id => @post.id ,:id => @post.vote.id, :vote => { :score => "Up" } } flash[:notice].should eq("You vote counted!") response.should redirect_to(post_path(@post)) end it "should not vote if you have already voted" do sign_in @user put 'update', {:post_id => @post.id ,:id => @post.vote.id, :vote => { :score => "Up" } } put 'update', {:post_id => @post.id ,:id => @post.vote.id, :vote => { :score => "Up" } } flash[:notice].should eq("You have already voted") response.should redirect_to(post_path(@post)) end it "should not vote if you can not abilitiy" do put 'update', {:post_id => @post.id ,:id => @post.vote.id, :vote => { :score => "Up" } } response.should redirect_to(post_path(@post)) @post.vote.score.should eq(0) end end describe "DELETE clear" do before(:each) { @user = Factory(:user) @user.roles << Factory(:role, :name => :admin) @post = Factory(:post, :user => @user) } it "should clear if admin" do sign_in @user delete 'clear', { :id => @post.vote.id, :post_id => @post.id } flash[:notice].should eq("Clear!") response.should redirect_to(post_path(@post)) end it "should not clear if user" do delete 'clear', { :id => @post.vote.id, :post_id => @post.id } response.should redirect_to(post_path(@post)) end end endТесты писал интуитивно, поэтому многие куски могут быть лишние. Например меня смущает Создание Ролей.
Вот и всё. Все 24 теста зелёные.
UPD: код на гугле: http://code.google.com/p/example-app/
Этот комментарий был удален автором.
ОтветитьУдалитьЗабыл добавить Post_id к таблице Vote
ОтветитьУдалитьи я вот сколько не смотрел ( я новичок в рельсах ) - но так и не понял для чего нужно post.try(:user) == user, почему нельзя
can :update, Post, :user_id => user.id
да, можно и так.
УдалитьХм, структура таблиц мне не очень нравится
ОтветитьУдалитьв Votes я бы хранил post_id | ip ну и стандартные таймштампы
а в Posts хранил бы счетчик голосований belongs_to :vote, :counter_cache => true
Так бы и быстрее (без join) сразу доставать кол-во голосов за пост, ну и например проверять накрутки можно было бы.
статья гавно
ОтветитьУдалить