Popular Posts

пятница, 13 апреля 2012 г.

Rails Autocomplete

Привет.
Сегодня рассмотрим автозаполнение (autocomplete) модная штука, с ajax. Для этого я буду использовать наработки. Помните пост, где делали теги? Вот именно для них я буду и делать автозаполнение.
И так у нас есть модель пост, с добавлеными туда тегами.
class Post < ActiveRecord::Base
    acts_as_taggable_on :tags
end
Конечно там больше кода, но важно видеть, что я не обманываю и там действительно есть теги.
Хорошо, теперь добавляем в контроллер метод для который будет отдавать все теги:
class PostsController < ApplicationController
  def get_tags
    @tags = Post.tag_counts.order('count DESC').limit(3).where('tags.name LIKE ?', "%#{params[:q]}%")
    render :get_tags, :layout => false
  end
end

Здесь простой запрос  к методу tag_counts, который автоматически генерируется acts_as_taggable_on, потом я ограничиваю количество тегов до 3-х, хотя можно и не ограничивать, и буду искать все имена тегов (tags.name), которые похожи на %params[:q]%,
это говорит о том, что искомый тег может начинаться и заканчиваться с любого символа. То есть  я буду отправлять запрос вида :q => "a", и вот эта буква "а" может встречаться в любом месте имени.
Ответ как видим не требует всего layout, потому что иначе будет отправлена в ответе вся страница со всеми файлами и прочим, нам же нужны только теги.

Добавляем, теперь этот самый шаблон страницы.
#views/posts/get_tags.html.erb
<ul>
<% @tags.each do |tag| %>
    <li><%= tag.name %></li>
<% end %>   
</ul>
Тут как видите просто, оборачиваем имя тега (name) в элементы li.
Я забыл сказать, что буду использовать Ajax.Autocompleter от Script.aculo.us. Вот, он требует такого ответа, хотя лучше ответ в json, но это тут не важно.

Затем добавляем в форму нового поста, поле для автозаполнения:
#views/posts/_form.html.erb
<%= form_for(@post) do |f| %>
  #несколько полей

<div class="field">
    <%= f.label 'Tag List' %><br />
    <%= f.text_field :tag_list, {:id => "autocomplete"} %>
    <div data-url="<%= url_for(get_tags_posts_path) %>" id="autocomplete_choices" class="autocomplete"></div>
</div>

<% end %>

Здесь я добавил атрибут data-url к полю для автозаполнения, этот url будет генерироваться автоматически, именно по этому адресу нужно обращаться для получения тегов.

Не забываем указать в ability.rb, кому можно получать теги:
#models.ability.rb
def initialize(user)
 #код

   if user.role?(:user)
        can :get_tags, Post

 #код
end
Это говорит о том, что теги могут получить только вошедшие в систему пользователи.
Так теперь добавим роутинг.
#config/routes.rb
resources :posts do
  #....

   post :get_tags, :on => :collection
end

Всё.

Осталось написать небольшой js-файл для обработки всего.
Сделайте файл в assets/javascripts/autocomplete.js
Вот нужный код

$(document).observe('dom:loaded', function() {
 var token = $$('input[name=authenticity_token]')[0];
 var url = $('autocomplete_choices').readAttribute('data-url');
 new Ajax.Autocompleter("autocomplete", 
                           "autocomplete_choices", url, 
                           {
                            paramName: "q",
                            tokens: [','],
                            requestHeaders: {'X-CSRF-Token': token.getValue()}
                           });

});

Переменная token нужна для предъявления серверу токена пользователя, это для защиты.
Потом мы читаем data-url, который добавили, помните? Это адрес для отправки запроса.
Потом сам Ajax.Autocompleter. У него первый аргумент -- это поле из которого нужно брать символы, второй это поле в которое будет писаться ответ, а потом идут параметры.
paramName: "q", q - это то, что будет в params, которое читаем в контроллере.
tokens: [','], это важный параметр, указывает разделитель между тегами, если его не будет, то будет на автозаполнении только первый самый тег, остальных нет.
requestHeaders, указывает дополнительный заголовок для сервера, тут отправляем token.getValue(), это значение находится в скрытом поле. Можете в исходном коде страницы посмотреть. Без этого заголовка, сервер не принимает нас, и перенаправляет на страницу входа, можете попробывать.
Теперь, нужно же добавить файл, так как автозаполнение нужно будет всего на двух страницах, странице создания и редактирования пост, то добавляем в _form.html.erb и всё.

<%= javascript_include_tag "autocomplete" %>

<%= form_for(@post) do |f| %>

#ваш код
Конечно это вставит код посреди страницы, но это не страшно.
Так последнее действие это добавление в загрузку нужных файлов.
#assets/javascripts/application.js

//
//= require scriptaculous/lib/prototype
//= require scriptaculous/src/effects
//= require scriptaculous/src/controls

Как видно я зугружая библиотеку прототип и нужные файлы из script.aculo.us -- эффекты и контролы. Поэтому разместите папку scriptaculous в assets/javascripts.

Ах да не забудьте добавить стили:

div.autocomplete {
  position:absolute;
  width:250px;
  background-color:white;
  border:1px solid #888;
  margin:0;
  padding:0;
}
div.autocomplete ul {
  list-style-type:none;
  margin:0;
  padding:0;
}
div.autocomplete ul li.selected { background-color: #ffb;}
div.autocomplete ul li {
  list-style-type:none;
  display:block;
  margin:0;
  padding:2px;
  height:32px;
  cursor:pointer;
} 

Ну или напишите свои, эти со страницы autocomplete.


суббота, 31 марта 2012 г.

Поиск в Rails (search in rails), используем sunspot

Привет

Давайте добавим поиск, у нас уже есть Post, Comment по которым можно что-то искать.
Для поиска я буду использовать Solr, который форкнут для rails Sunsop Solr.
и так начнм с установки в наш Gemfile:
gem 'sunspot_rails' #форк для rails
gem 'sunspot_solr'  #сам Solr
gem 'progress_bar'  #нужен для индексирования
group :test, :development do #в группу для разработки и тестирования
  gem "sunspot-rails-tester" #это нужно для запуска тестов, иначе не работает.
  #...
end
Ну и делаем bundle install, для установки gem'ов.
Хорошо теперь в модели нашей в Post, по которой и будет поиск добавляем след. блок:
class Post < ActiveRecord::Base
  after_create :reindex!
  after_update :reindex!

  searchable do
    text :title, :content
    integer :user_id
    time    :created_at
  
    string :sort_title do
     title.downcase.gsub(/^(an?|the)/, '')
    end
  end
  #...
  
  protected

    def reindex!
      Sunspot.index!(self)
    end
end
Блок searchable нужен для описания наших полей, тут можно добавить больше или наоборот убрать больше читайте в вики.
Тип text указывает, что это текстовые поля, time -- время. Здесь понятно, этот блок нужен для полнотекстового поиска по модели.
Также я добавил два callback'а, это нужно для того, чтобы когда мы добавим новый пост, был произведён переиндексирования, тоже
самое и для обновления поста. Какие здесь подводные камни? Нет переиндексации для комментариев, да?
Так теперь сделаем отдельный контроллер для поиска, хотя можно использовать и PostsController, но я лучше вынесу в отдельный
контроллер.
rails g controller search
#controllers/searc_controller.rb
class SearchController < ApplicationController

  def search
    @post = Post.search {
      keywords params[:query]
      order_by :created_at, :asc
    }.results
    render 'posts/index'
  end
end
Поиск делается методом search, который принимает блок, что в блоке, смотрите в вики, мне хватило, чтобы было
отсортировано по дате, а слова поиска указываются в параметрах. Ну и так как своего представления нет для
этого контроллера, то показываем на index из контроллера Posts.
Теперь сделаем форму поиска:
#posts/_search.html.erb
<%= form_tag search_path, :method => :get do %>
  <p>
    <%= text_field_tag :query, params[:query] %>
    <br />
    <%= submit_tag "Search!" %>
  </p>
<% end %>
Вот простая форма для поиска.


Теперь добавим для тестов несколько строк:
#spec/spec_helper.rb
$original_sunspot_session = Sunspot.session

RSpec.configure do |config|
 
  config.before do
    Sunspot.session = Sunspot::Rails::StubSessionProxy.new($original_sunspot_session)
  end
    config.before :each, :solr => true do
    Sunspot::Rails::Tester.start_original_sunspot_session
    Sunspot.session = $original_sunspot_session
    Sunspot.remove_all!
  end
  #......
  #......
  #......
end
Вот теперь всё прекрасно.
Как запускать?
rake sunspot:solr:start
#=>java version "1.6.0_23"
   OpenJDK Runtime Environment (IcedTea6 1.11pre) (6b23~pre11-0ubuntu1.11.10.2)
   OpenJDK Server VM (build 20.0-b11, mixed mode)
   Removing stale PID file at /home/mike/app/example-app/solr/pids/development/sunspot-solr-development.pid
   Successfully started Solr ...

Как остановаить?
rake sunspot:solr:stop
#=>java version "1.6.0_23"
   OpenJDK Runtime Environment (IcedTea6 1.11pre) (6b23~pre11-0ubuntu1.11.10.2)
   OpenJDK Server VM (build 20.0-b11, mixed mode)
   Successfully stopped Solr ...



сам gem на гитхабе
тоже
вики
railscasts

Полиморфные отношения. (Polymorphic Association)


Привет.
Сегодня рассмотрим как использовтаь полиморфные отношения в rails.
С чем это можно сравнить? Помните в детсвет у вас был карандаш\фломастер, которым
вы изрисовали пол\обои\стол\руки. Так вот карандаш -- это и есть полиморф, то есть рисует и на полу и на столе.
Так и здесь это такое отношение, которое может быть для многих типов.
Там во второй части я сделал модель для голосования, помните?
Вот это и будет наша полимофрная модель Vote.
и так сперва делаем миграцию:
rails g migration add_type_into_vote
Пишем в получившийся класс:
class AddTypeIntoVote < ActiveRecord::Migration
  def change
    change_table :votes do |f|
      f.string :votable_type
    end
    rename_column :votes, :post_id, :votable_id 
  end
end
Здесь как видно добавляем колонку для типа, здесь будет записан тип к которому применяется
модель. У нас это Post и Comment. То есть позволяет ставить голоса за комментарии и за посты.
Теперь исправим классы моделей.
#models/vote.rb
class Vote < ActiveRecord::Base
   belongs_to :votable, :polymorphic => true
   #....
end
Это описывает полиморфную модель, указываем строчкой polymorphic => true, то что это полиморфная модель,
а также смотрите belongs_to, не к post\comment, а к votable.
Затем мы могли бы добавить в каждую модель (Post, Comment) одинаковый код:
after_create :create_vote
has_one :vote, :dependent => :destroy, :as => :votable

def create_vote
  self.vote = Vote.create(:score => 0)
end
Видите, это должно быть в каждой модели, но это нарушает принцип DRY. Поэтому создаём модуль в директории с моделями:
#models/vote_module.rb
module VoteModule
  def self.included(base)
    base.module_eval {
      after_create :create_vote
      has_one :vote, :dependent => :destroy, :as => :votable
    }
  end

  protected
    def create_vote
      self.vote = Vote.create(:score => 0)
    end
end
Тут применено метапрограммирование self.included(base)
base - это класс в который подмешивается модуль.
self.included будет срабатывать после вставки в класс, и включать макросы after_create и has_one.
Смотрите в has_one указывается не votable, а vote, но то что это полиморфная модель говорит опция :as
Теперь это нужно включить в классы Post и Comment
#models/post.rb
class Post < ActiveRecord::Base
  include VoteModule
   #....
end
-----
#models/comment.rb
class Comment < ActiveRecord::Base
  include VoteModule
  #...
end
Хорошая техника, правда?
Так хорошо, исправляем контроллер:
#controllers/vote_controller.rb
class VoteController < ApplicationController
  load_and_authorize_resource
  before_filter :type
 
  def update
    vote = @type.vote 
    vote.score += 1 if params[:vote] == "Up"
    vote.score -= 1 if params[:vote] == "Down"
    vote.users << current_user
    vote.save

    message = "You vote counted!"
    redirect(message)
  end

  def clear
    vote.update_attributes(:score => 0)
    message = "Clear!"
    redirect(message)
  end

  rescue_from CanCan::AccessDenied do |exception|
    message = "You have already voted"
    redirect(message)
  end
 
  private
   
    def type
      @type = Post.find(params[:post_id])
      @type = Comment.find(params[:comment_id]) if params.include?(:comment_id)
    end
   
    def redirect(msg)
      respond_to do |format|
        format.text { render text: msg }
      end
    end

end
Как-то так, получилось (я уже забыл на самом деле, что там, и этот контроллер уже переписан, но про это в след. постах)
Однако тут мы сперва используем фильт before_filter, чтобы установить тип модели, или это пост или комментарий. Вот и всё
что важно в этом контроллере.
Представление исправте сами (ДЗ, ага). Не забывайте про роутинг:
#config/routes.rb
resources :posts do
    resources :vote, :only => [:update] do
      delete 'clear'
    end
   
    resources :comments, :only => [:create, :destroy] do
      resources :vote, :only => [:update] do
        delete 'clear'
      end
    end
end
Получается Vote вложен в post и comment.
Когда будете формировать линки для учитывайте это, посмотрите, что должно быть rake routes.
Вот как бы и всё про полиморфные модели, больше информации в api.

четверг, 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

вторник, 6 марта 2012 г.

Devise + Cancan -- быстрая разработка. Часть вторая.

Привет.
Продолжаем, обещанное вчера -- 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/


понедельник, 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/


Прошлый пост

вторник, 28 февраля 2012 г.

PrototypeJS Классы, Наследование и Модули.

Привет!
Сегодня важная тема, я бы сказал -- это наследование и классы и объекты в prototype, ну также всё, что будет показано относится в равной мере и ко всему javascript.

Проблема

Как всегда сначала описание проблемы. Предположим у меня есть мебель: шкаф и диван. Ну да немного, но для задачи достаточно. Как это выглядит со стороны объектов? Понятно, что и то, и то -- мебель, ведь так? Но на диване я сижу, лежу, и так далее, а в шкафу куча дверек, в шкаф я складываю одежду\посуду\да_что_угодно. Но также есть такие общие вещи как цвет. Вот.
Теперь непосредственно проблема, по идее диван и шкаф виды мебели, но диван и шкаф могут иметь разные свойста (для сиденье? сколько двёрок? и так далее). Как же это описать?
Вот тут-то и появляются так называемые MixIn, или модули, которые могут быть в новинку для javascript программистов, но отнюдь не новы для тех, кто пишет на ruby. Но благодаря, природе javascript в нём можно спокойно делать модули и разбавлять объекты. Я написал много слов, теперь напишу код.
И так. Сначала практика создания классов в prototypejs
Тут просто:
var SomeClass = Class.create({
    initialize: function(){}
});
Как просто, да? Сначала делаем класс Class.create, в нём нужна функция конструктор (но она не обязательна) initialize, что сродни ruby методу с таким же названием. Это функция будет инициализироваться при создании объекта. Всё просто.
Всё теперь пишу нужный код:
var Furniture = Class.create({
    initialize: function(title, price)
    {
      this.title = title;
      this.price = price;
    },
    toString: function()
    {
          return this.title + ": " + this.price + "$"
    }
    });
Две функции, инициализации, и вторая функция для вывода информации о мебели.
Этот класс в java я бы сделал абстрактным, потому что просто мебели как бы не существует, это абстаркция, мебель это всегда что-то -- стул, стол, шкаф...

Теперь пишем классы для Шкафа и Дивана:
var Sofa = Class.create(Furniture, {
    initialize: function($super, title, price)
    {
      $super(title, price);
    }
});
var Cupboard = Class.create(Furniture, {
    
    initialize: function($super, title, price)
    {
      $super(title, price);
    }
});
Как вы, надеюсь, поняли, здесь мы наследуем от мебели (Furniture) с помощью Class.create(ExtObj, {}), где ExtObj -- это "родитель". В инициализаторе вызываем $super, это
заставляет проинициализировать код функции родителя. Про $super, читайте в конце поста.

Ok, я это сделал, теперь время подмешать модули.
Пусть есть такие свойста: цвет, можно ли сидеть, сколько дверек в мебели.
//для цвета
var Color = {
      set_color: function(color)
      {
        this.color = color;
      },
      get_color: function()
      {
        return this.color;
      }
};
//Я могу сесть на эту мебель?
var ForSeating = {
    for_seating: function()
    {
      return true;
    }
};
//Сколько дверек в этой мебели?
var ManyDoors = {
    set_doors: function(doors)
    {
        his.doors = doors;
    },
       how_many: function()
    {
      return this.doors;
    }
};
Ok, круто, как видим все эти модули просты Object\ы  в javascript.
Теперь самое интересное и простое, как подмешать? Для этого есть методы Object, который расширяет Prototype -- addMethods()
Получаем:
//Для дивана модуль который говорит, что можно сидеть и модуль цвета
Sofa.addMethods(ForSeating);
Sofa.addMethods(Color);
//Для шкафа цвет и дверки.


Cupboard.addMethods(Color);
Cupboard.addMethods(ManyDoors);
Что получим?

Вот результаты:
Сначала создаём новые:
var sofa = new Sofa('Cool Sofa', 10);
var cupboard = new Cupboard('My Cupboard', 12);


//Устанавливаем цвета и двери для шкафа
sofa.set_color('red');

cupboard.set_color('black');
cupboard.set_doors(3);

//А вот результат уже
sofa.get_color(); // => 'red'
sofa.for_seating(); // => true
cupboard.get_color(); // => 'black'
cupboard.how_many(); // =>  3
Если мы вызовем для шкафа метод, который скажет можно ли на нём сидеть? то получим



Вот так.

Совсем маленький рефакторинг.

Как мы видили, Модуль Color добавляется и в Sofa и в Cupboard, поэтому лучше "подмешать" в родительский класс:

Furniture.addMethods(Color);

Теперь методы этого модуля будут доступны в обоих объектах.

Для чего всё это нужно?

Это позволяет вынести общие методы для разных объектов, что упростит жизнь, ведь кода нужно меньше, вы не будете повторять себя.
Сделает понятнее программу.
Какие возможные траблы?
Возможно затереть методы, очень часто есть методы set()\get(), которые могут быть реализованы для разных модулей, в итоге останется только один, как горец))). Также сложность следить за программой, у вас в модуле появится ссылка this, на объкт + куча свойств объекта, не сразу понятно как этим пользоваться, но это просто, так же в связи с этим можно случайно переопределить свойство, поэтому рекомендую не полениться и написать get_property()\set_property(), так лучше.

Я написал много слов об недостатках, но это для того, чтобы предупредить о сложности, на самом деле плюсов больше.

Примечание: Наследование PrototypeJS

Снова немного кода, для пояснения некоторых вещей.  Есть несколько классов:
var A = Class.create({
    say: function()
    {
      alert('A');
    },
    method: function()
    {
      alert('method A');
    }
    });
    var B = Class.create(A, {
    say: function()
    {
      alert('B');
    }
    });
    var C = Class.create(A, {
    say: function($super)
   {
      $super();
      alert('C');
    },
    abc: function()
    {
      alert('abc');
    }
    });
   
    var a = new A();
    var b = new B();
    var c = new C();
    a.say(); // => A
    b.say(); // => B
    c.say(); // => A, C
    c.method(); // => method A
Здесь видим наследование B и C наследует A. Но в B мы пишем свой метод say(). а в C вызываем сначала родительский $super() -- это пригодится, когда нужно установить некоторые свойста, которые определяются в родительском, например при инициализации. Также видим, что метод method() из A доступен и в C дочернем классе.  Доки






суббота, 11 февраля 2012 г.

BDD в javascript. Jasmine

Сегодня я начну серию постов о тестировании Javascript кода. Многие из разработчиков уделяют этому мало времени, считая, что это не стоит внимания, потому что javascript -- это детский язык, который можно выучить за пару дней. Это конечно же ошибка, чтобы понять суть javascript  у некоторых уходит достатточно много времени,  и они крайне удивляются особенностям javascript, например:
 "1" + 3 == 13 // true
 "10" - 3 == 7 // true
Я напишу несколько постов сначала jasmine, затем фреймворк для Prototypejs & scriptaculous. В серии jasmine будет сначала просто работа с jasmine, потом работа с ajax & DOM, потом интерграция c prototypejs и jquery. Jasmine из Ruby, Потом уже Unit тесты от Томаса Фукса (script.aculo.us)
Но это совсем не значит, что я не буду писать о другие посты.
и так поехали.

Jasmine -- это BDD фреймворк для тестирования javascript кода. Довольно простой, не зависит от других фреймворков, то есть был написан для javascript кода, а не для prototype\jquery\yui.

Синтаксис.


Jasmine унаследовало или скорее поддерживает синтаксис основного bdd фреймворка -- RSpec. Тесты состоят из двух частей describe() -- указывает поведение, какой-то общий контекст для которого выполняются тесты. Вторая часть -- it(), простое утверждение. Describe() и it() принимают два аргумента, первый -- это строка, которая показывае, что вы делаете. Второй -- это функция, непосредственно тест.

Проект

Допустим мы скачали jasmine с сайта. Теперь посмотрим какова структура проекта (директорий).
- jasmine-standalone
   -- lib
      --- jasmine-1.1.0
           ---- некоторые файлы
  -- src
     -- //ваши файлы тестируемые
 -- spec
     -- // ваши тесты
spec_runner.html //запуск тестов, в этот файл скопируйте просто отсюда и пропишите свои пути к файлам тестов и своего кода.

Вот достаточно просто. Для запуска тестов просто открываем spec_runner.html в  любом браузере. Смотрим какие прошли, какие не прошли и исправляем.

Написание тестов

Но прежде, чем тестировать нужно что-то написать для тестов. У меня есть вполне реальный пример кода, который я использую, это определитель високосного года.
function isLeapYear(year) {

       if (year % 4 == 0) {
                if (year % 100 == 0) {
                        if (year % 40 == 0) {
                                return true;
                       }
                     return false;
                }
                return true;
       }
      return false;
}; 

Работа понятна принимает значение года, возратит true или false, в зависимости от принятого года.

Вот теперь можно и писать тесты:
describe("isLeapYear", function() {
    it("2004 should be leap year", function() {
        expect(isLeapYear(2004)).toBeTruthy();
        expect(isLeapYear(2004)).toEqual(true);
    });
   it("2000 should be leap year", function() {
      expect(isLeapYear(2000)).toBeTruthy();
   });
   it("1700 should not be leap year", function() {
      expect(isLeapYear(1700)).toBeFalsy();
   });
   it("2001 should not be leap year", function() {
     expect(isLeapYear(2001)).toBeFalsy();
   });
});
Как видим в describe я указал, что будут тесировать функцию isLeapYear, хотя тут можно было указать что угодно. А потом идут утверждения it(). В блоке которых я проверяю с expect(), за expect() идёт некотрое утверждение (matchers), которых несколько в jasmine. Всё довольно просто. Но наши тесты не могут быть полными, ведь я могу отправить вместо числа строку или массив или ничего, поэтому улучшаем функцию.
function isLeapYear(year) {
   if (typeof(year) != 'number')
           throw 'Year should be Number';
 ...
}
Если тип аргумента не номер, будет брошено исключение.
Дописываем необходимые тесты:
it("Not_Number should throws exception", function() {
    expect(function() { isLeapYear('Not_Number'); }).toThrow('Year should be Number');
    expect(function() { isLeapYear(); }).toThrow('Year should be Number');
});
Всё отлично работает:

beforeEach & afterEach

Сейчас рассморим работу только beforeEach, так как afterEach будет  работать также. Сейчас я напишу некоторую функция-класс, которая будет делать, то же что и isLeapYear:
var Year = function() {
    this.isLeapYear = function(year) {
            return isLeapYear(year);
    };   
};
Видим класс возвращает просто функцию isLeapYear.
Напишем тесты, но уже  с beforeEach:
describe("Object Year.isLeapYear", function() {

   beforeEach(function () {
          Obj = new Year();
     });

     it("2004 should be leap year", function() {
     expect(Obj.isLeapYear(2004)).toBeTruthy();
   });
});
В beforeEach устанавливаем Obj, который будет экземпляром Year. А в блоке it(), проверяем метод этого объекта Obj.isLeapYear(). Я не писал все тесты, потому что они будут подобны прошлым. Вот результат:

Пишем свой mathcer

Написание свойго утверждения в jasmine довольно просто. Может возникнуть зачем новый mather? Всё довольно просто -- это устраняет дублирование кода, а также улучшает кода, ведь не нужно писать большие манипуляции, а достаточно написать один раз новый matcher.
Matcher -- это функция, которая будет принимать сколько угодно аргументов (всё зависит от того, что вы проверяете) и манипулирует переменной this.actual -- это поступает из expect() блока. Matcher добавляется с помощью this.addMatchers({}) в блоке beforeEach(), возвратить ваш matcher должен либо true, либо false, и так когда есть немного теории.
beforeEach(function() {
    this.addMatchers({
         toBeLeapYear: function () {
            var year = this.actual;

                       if (typeof(year) != 'number')
                          throw 'Year should be Number';

                      if (year % 4 == 0) {
                      if (year % 100 == 0) {
                         if (year % 40 == 0) {
                             return true;
                         }
                         return false;
                      }
                      return true;
                   }
                   return false;
                  } 
    });
});
Как видим -- это наша старая функция isLeapYear().
Теперь спеки:
it("2004 should be leap year", function() {
    expect(2004).toBeLeapYear();
    expect(2000).toBeLeapYear();
});
it("2001 fail test", function() {
    expect(2001).toBeLeapYear();
});

Здесь один тест не пройдёт, что и ожидалось.

---------------------
Что не было рассмотрено: шпионы и асинхронные спеки









четверг, 9 февраля 2012 г.

Немного о BDD

   Сейчас совсем короткий пост о BDD.
   Что подрузомевает под собой эти 3 буквы)))
  Всё просто, как всегда -- это техника разработки программ, буквально  разработка программ по их поведению.
   Другая техника -- это TDD, вот как можно описать TDD:
  С чем это можно сравнить, это можно сравнить с тем, как идёшь в темноте в незнакомом
месте. Ударился -- это плохо (тесты не прошли), повторно, здесь уже не ударишься (рефакторинг).
Снова повторно проходим, так до тех пор пока не выйдем (тесты все зелёные).
  BDD же -- это
- установка целей, то есть, что будем делать.
- начинаем писать
- пишем примеры к коду, к каждому методу, буквально к каждой строчке -- это и будет
поведение
- автоматизация примеров.
   Другими словами, если использовать ту же аналогию тёмной комнаты.
- цель -- выйти
- начинаем идти, тут есть за что зацепиться и упасть
- падаем\не_падаем, показывая, где упадём, а где нет.

   Время для примера, после стольких слов.  
  Но сначала, замечание. Сейчас я напишу класс, который в зависимости от количества переданых сторон будет показывать -- это треугольник, квадрат, или пятиугольник. Если количество переданых сторон меньше 3 или больше 5, то будет бросаться исключение. Здесь я не буду рассматривать такие зависимости, что, например в треугольнике сумма двух сторон должна быть больше третье, или что у стороны не может быть нулевой длины, или что у квадрата все стороны равны. 


#example.rb
#включаем в файл необходимую нам библиотеку (RSpec)
require 'rspec'

#сначала класс исключений:

class ShapeExeption < Exception 
end

#сам класс для фигур

class Shape
     attr_reader :iam

     TRIANGLE = "Triangle"
     QUADRATE = "Quardate"
     PENTAGON = "Pentagon"

     def initialize(*args)
         @iam = case args.size
               when 3
                   TRIANGLE
               when 4
                  QUADRATE
               when 5
                  PENTAGON
           end   
         raise ShapeExeption.new "What is this shape?" if @iam.nil?         
      end
end

----------------------------------------------------------------------------
   Сейчас поговрим про этот класс. 
  Вначале я определил, что переменная @iam будем только читаться, но пеопределить её нельзя.
   Затем определил констатны, для треугольника\квадрата\пятиугольника.
  Потом метод инициализации, который принимает какое-то количество аргументов, это показывается *. 
   Потом в блоке case...end определяем, длину полученых аргументов. Ну и для каждой длины своя констатнта, которая укажет, какая фигура.
  В конце метода, есть вызов исключения ShapeException, это когда @iam не получена, в принципе, это же можно было бы и включить в блок case, но я не стал.
  А теперь проверим поведение всего этого кода.

describe "Shape" do

    it "should be Triangle with three arguments" do
          shape = Shape.new(1, 2, 3)
          shape.should be_an_instance_of(Shape)
          shape.iam.should eq(Shape::TRIANGLE)
   end

   it "should be Quardate with four arguments" do
         shape = Shape.new(1, 2, 3, 4)
         shape.should be_an_instance_of(Shape)
         shape.iam.should eq(Shape::QUADRATE)
   end

   it "should be Pentagon with five arguments" do
         shape = Shape.new(1, 2, 3, 4, 5)
         shape.should be_an_instance_of(Shape)
         shape.iam.should eq(Shape::PENTAGON)
   end

    context "Exeption" do
        it "should raise ShapeExeption if arguments not equal [3,4,5]" do
            expect { Shape.new }.to raise_error(ShapeExeption)
            expect { Shape.new(1,2,3,4,5,6) }.to raise_error(ShapeExeption)
        end
     it "should raise NoMethodError when write @iam" do          shape = Shape.new(1, 2, 3)          expect { shape.iam = "abc" }.to raise_error(NoMethodError)      end   end end
   Первой строчкой describe Shape, показывается, что будем проверено поведие класса. 
   Затем идут три метода it(), в каждом проверяется, что наша переменная shape инициализирована  от класса Shape, и проверяем, что возвратит метод iam.
   Потом в контектсе (context) проверяем исключения.
У нас их две, когда значений передано меньше или больше, и когда пытаемся перезаписать @iam.
Это проверяется в блоках expect{}.to raise_error().
   Запускаем:

rspec example.rb --color

.....

Finished in 0.00201 seconds
5 examples, 0 failures

   Вот таким несложным образом было проверяно поведение. 
   Да это программа немного надумана, но отражает действительную суть. Этот класс может быть использован для возврата соответвенно объектов Треугольник\Квадра\Пятиугольник\N-угольник.

среда, 8 февраля 2012 г.

Acts-as-taggable-on -- делаем тэги.

Наверное одной из самых популярных вещей, которые реализуют буквально все, это тэггирование постов, фотографий, всего. Тэги помогают лучше ориентироваться, ведь если что-то находится в категории например "javascript", то это совсем не значит, что во всех постах будет описываться работа с ajax или с prototypejs. Поэтому тэги помогают отобрать из пачки постов\фотографий нужные.
Как же это делается в rails?
Всё просто, есть gem acts-as-taggable-on, который позволяет расставлять тэги. Я покажу как это делается для постов.
Что у нас есть? Во-первых -- это модель Post + контроллер + представления.
Всё это сгенерировано
$ rails g scaffold post title:streing,  content:text
Затем устанавливаем сам gem в Gemfile, просто добавляя строчку:
gem 'acts-as-taggable-on'
и выполняем bundle install. Всё установлено.
Затем добавляем в модель следующее:
class Post < ActiveRecord::Base
  acts_as_taggable_on :tags   # <= это добавили
  attr_accessible :title, :content,  :tag_list # <= и здесь записываем tag_list, если не запишем, то не сможем посмотреть тэги, будет вызвана ошибка.
end
В контроллер также добавляем метод.
class PostsController < ApplicationController
    #для вывода облака тэгов
    def tag_cloud
        @tags = Post.tag_counts_on(:tags)
    end
  
   def index
       @posts = Post.all
       tag_cloud  # <= отсюда обращаемся, чтобы получить все тэги
   end
end
Всё, теперь в представлении, я добавил, в application.html.erb, это для того, чтобы видно было на всех страницах, если вам нужно, чтобы облако было только на какой-то определённой странице, добавляйте в представление той страницы.
<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
          <%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
<% end %>


css1 css2 css3 css4 -- классы стилей для тэгов, от большого до маленького.

Вот собственно и всё. Проще, чем кажется на первый взгляд.
Документация по gem'у покажет некоторые хорошие вещи, которые можно сделать.

преведущий пост


четверг, 2 февраля 2012 г.

Ruby Koans

   Многие сталкиваются с проблемой, что прочитам несколько книг, и не почти не написав  ни строчки кода(!), хотят проверить свои знания.
   Для этого есть один хороший проект нацеленый на это это Ruby Koans.
   Проект построен на базе тестов, нет не тех тестов где нужно выбрать один вариант из нескольких, а TDD (то самое про которое писал Кент Бек).
   В koans нужно выполнить 280 тестов, каждый тест содержит несколько "утверждений" (assertion). Например простейший тест:
def test_creating_arrays
      empty_array = Array.new
      assert_equal __, empty_array.class
      assert_equal __, empty_array.size
end
Вместо __ нужно вписать правильные ответы:
def test_creating_arrays
      empty_array = Array.new
      assert_equal Array, empty_array.class
      assert_equal 0, empty_array.size
end
Таким образом нужно освоить 280 тестов, но там не только нужно вписывать свои ответы, также есть возможность написать свои классы, то есть есть на чём потренироваться в написании кода.
В конце увидите, если все 280 тестов правильны:


Last post

четверг, 12 января 2012 г.

Печём для моделей. Тестируем модели в rails

Привет.
Сегодня я покажу, как протестировать модели в rails. Для этого мы будем использовать RSpec - специальная утилита для Ruby. Сделана для BDD.
Сперва устанавливаем необходимые gem'ы.
gem install rspec
gem install factory_girl_rails
gem install rspec-rails
gem install shoulda-matchers
Первых два gem'а это непосредственно rspec и rspec для rails. Третий gem - это фабрика, которуб будет использовать вместо фикстур. Последний для тестирования ассоциаций.


и так у меня есть две модели: Post и Comment.
Таблицы для них элементарные:
- posts
   - title
   - content
 - comments
   - name
   - comment
   - post_id


Пост может иметь много комментариев - зависимость has_many.
Также я проверяю, чтобы все поля были заполнены - валидатор presence, а также для названия поста проверяю длину (от 3 символов).


Таким образом классы:
#/models/post.rb
Class Post < ActiveRecord::Base
   validates :title, :presence => true
   validates :content, :presence => true
   validates :title, :length => { :minimum => 3 }
#асосиации
   has_many :comments
end

#/models/comment.rb
Class Comment < ActiveRecord::Base
   validates :name, :presence => true
   validates :comment, :presence => true
#связываем 
   belongs_to :post
end

Всё, модели готовы.


Теперь напишем элементарные тесты, но для начала нужно включить установленные gem'ы в Gemfile:


#Gemfile
group :test do
  gem "rspec" 
  gem "rspec-rails"
  gem "factory_girl_rails"
  gem "shoulda-matchers"
end

Сейчас генерируем спеки:
rails g rspec:install

Эта команда генерирует директории для контроллеров, моделей, хелперов и так далее.


Отлично. Теперь самое интересное пишем тесты спеки.


#/spec/models/post_spec.rb
require 'spec_helper'

describe Post do
  #сначала проверяем валидаторы
  context "validates" do
     before(:each) do
        @post = Factory.create(:post)
     end
     
     #должна быть валидная, если валидные атрибуты (title, content)
     it "is valid with valid attributes" do
       @post.should be_valid
     end
     
     #не валидная без title   
     it "is not valid without a title" do
        @post.title = nil
        @post.errors_on(:title).should include("can't be blank")  
        @post.should be_valid
     end
     
     #не валидная без content'а
     it "is not valid without a content" do
               @post.content = nil
               @post.errors_on(:content).should include("can't be blank")
               @post.should_not be_valid
          end
     
      #не валидная, если длина названия, меньше чем 3 символа
           it "is not valid if length of title less then 3 characters" do
               @post.title = "12"
               @post.should_not be_valid
           end
      
      #валидная с длиной больше, чем 3 символа
           it "is valid if length of title greater then 3 characters" do
               @post.title = "123"
               @post.should be_valid
           end
   end
end
Что я использовал здесь.
Сперва я подписался на Post, (describe Post)
Затем я разбил всё на контексты (context), первый это валидаторы.
Затем методами it, я проверял утверждения, например: Длина названия должна быть больше 3 символов - это просто утверждение, проверяется в методе it. Но перед вызовами каждого теста, я устанавливал @post - это происходит в методе before(:each). Проверка происходит методами RSpec, should, should_not (должен и не должен). Например:


      #валидная с длиной больше, чем 3 символа
           it "is valid if length of title greater then 3 characters" do
               @post.title = "123"
               @post.should be_valid
           end


будет звучать так: Если длина названия содержит более чем 3 символа, то пост валиден.


Тоже самое пишем и для модели Comment


#/spec/models/comment_spec.rb
describe Comment do
   context "validates" do 
       before(:each) do
             @comment = Factory.create(:comment)
        end

   
        it "is valid with valid attributes" do
              @comment.should be_valid
          end

          it "is invalid without name" do
             @comment.name = nil
             @comment.should_not be_valid
          end

          it "is invalid without comment" do
             @comment.comment = nil
             @comment.should_not be_valid
          end
     end
 end
end


Как видем тоже самое, проверка имени и комментария.
Что дальше, теперь напишем тесты для ассоциаций, всё это можно было бы сделать и без shoulda, но с ним более удобно. Но не удобно (для меня) тестировать валидаторы, поэтому я применяю этот gem для тестирования ассоциаций.
Добавляем контексты


#/spec/models/post_spec.rb

context "associations" do
    it "should have many comments depending" do
               should have_many(:comments) #пост имеет много комментариев
         end
end
#/spec/models/comment_spec.rb
context "associations" do
       it "should belongs_to post" do
          should belong_to(:post) #комментарии должны быть связаны с постом
       end
end
Теперь, что с Factory Girl (хорошее название)
фактори делает не нужными фикстуры, кто с ними работал, тот знает, что написание фикстур это утомительно, очень утомительно.


#/spec/factories/factoryes.rb

FactoryGirl.define do
   factory :post do
       title "Title"
       content "Content"
   end
end

FactoryGirl.define do
   factory :comment do
        name "Foo"
        comment "Bar"
        post
   end
end
Когда мы делаем Factory.create(:post), то получаем модель с  задаными атрибутами. Всё просто, не фикстуры.


Теперь запускаем тесты, так как я написал спеки только для моделей, то и запускать буду для моделей:
rake spec:models
===>
/home/xxx/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S rspec ./spec/models/post_spec.rb ./spec/models/comment_spec.rb
..........


Finished in 0.13836 seconds
10 examples, 0 failures


 
Ссыли:


https://github.com/thoughtbot/shoulda-matchers
https://github.com/rspec/rspec-rails
https://github.com/thoughtbot/factory_girl

понедельник, 9 января 2012 г.

scriptaculous. Делаем свой эффект.

Привет!

Сейчас давайте рассмотрим работу, пожалуй одной из самых распространённых библиотек для javascript - scriptaculous.
Scriptaculous надстройка над другим фреймворком - prototypejs.

Она состоит из частей:
  • Effects - включает различные эффекты и также помощники.
  • Behaviours - это drag, drop, sort, и работа с формами.
  • Controls - аякс и автокомлитеры
  • Miscellaneous - это "другое" или остальное, сюда входят тесты (unit testing), звук (sound), создание элементов и узлов из элементов (builder)
Всё это можно найти на сайте и потом я расскажу о каждой из частей.

Сейчас же я покажу как работать с эффектами, вернее не работать, а как создать собственный эффект.
Но сначала пару слов об эффектах, ну то есть немного теории.
Все эффекты наследуются от Effect.Base.
Наследуемый эффект может определить 3 необязательных метода:

  • setup() - этот метод вызывается до начала выполнения эффекта, и делает необходимые настройки, например там можно определить, некоторые нужные переменные, и так далее.
  • update(pos) - вызывается для каждой итерации. Аргумент pos - это текуший кадр. 
  • finish() - вызовется после выполнения эффектов.
Как я говорил, все методы не обязательны, поэтому наиболее часто вызывается update(pos), в котором и происходит вся "магия" эффекта.

Сейчас перейдём от теории к практике. Сделаем свой эффект.
Effect.MyEffect = Class.create(Effect.Base, {
/*   

initialize - метод-конструктор для эффекта, принимает аргументы<

   element - это id элемента.

    setting - необязателен, я его упомянул для того, чтобы показать, что он есть, это также дополнительные настройки, которые могу использоваться в вашем эффекте.

*/ 
    initialize: function(element, setting)
   {
      this.element = $(element);
    //бросаем исключение, если элемент не найден.
      if(!this.element) throw(Effect._elementDoesNotExistError); 
      this.start(setting);
   },
  setup: function()
  {
    Effect.tagifyText(this.element); //разбиваем элемент
    this.letters = this.element.select('span');
  },
  update: function(position)
  {
     var F = position < 0.5 ? 2*position: 2*(1-position);
     var f_pos;
     var s_pos;
     this.letters.collect(function(letter, j){    
        f_pos = Math.sin(position)*15*F;
        s_pos = - f_pos;
        if(j%2==0)
           letter.setStyle({top: Math.round(f_pos) + 'px'});
        else
           letter.setStyle({top: Math.round(s_pos) + 'px'});
     });
 }
});
Вот и всё! Эффект готов. Для примера, этот эффект принимает id элемента, например, заголовка <h1>Lorem ipsum dolore</h1> Но также можно передать и Id параграфа, тогда будет веселее. А сейчас более подробно. В методе-конструкторе мы принимает id, и оборачиваем его $(), если элемент не найден будем бросать исключение. В методе setup, с помощью tagifyText, мы оборачиваем каждую букву элемента в span. Затем мы просто отбираем все элементы. Как и говорилось в update() вся магия, но суть в чём, если чётное в одну сторону, если нет, то  в другую. Формула, которая я использовал, не несёт в себе какой-либо смысловой нагрузки, поэтому понять не пытайтесь. (Хотя на самом деле это простая формула синуса). Затем мы на полученых значениях сдвигаем либо вверх, либо вниз отобранные span'ы. Вот и всё. Просто же.