Popular Posts

Показаны сообщения с ярлыком rails. Показать все сообщения
Показаны сообщения с ярлыком rails. Показать все сообщения

пятница, 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.


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

среда, 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'у покажет некоторые хорошие вещи, которые можно сделать.

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


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