Привет.
Продолжаем, обещанное вчера -- 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
и вот для Voterequire '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) сразу доставать кол-во голосов за пост, ну и например проверять накрутки можно было бы.
статья гавно
ОтветитьУдалить