Опубликован: 27.01.2016 | Уровень: для всех | Доступ: платный
Лекция 10:

Микросообщения пользователей

< Лекция 9 || Лекция 10: 123456 || Лекция 11 >

Предварительная реализация потока сообщений

Комментарий в конце Раздела 10.3.2 намекает на проблему: текущая Home страница не отображает микросообщений. Если вы хотите, вы можете убедиться что форма, показанная на рис. 10.11 работает, введя допустимое микросообщение и затем перейдя на страницу профиля чтобы увидеть сообщение, но это довольно громоздко. Было бы гораздо лучше иметь feed (поток, канал) микросообщений, который включал бы в себя микросообщения пользователя, как показано на рис. 10.13. (В Главе 11 мы обобщим этот канал (поток), включив микросообщения пользователей за которыми следит текущий пользователь.)

Набросок страницы Home с предварительной реализацией потока сообщений.

Рис. 10.13. Набросок страницы Home с предварительной реализацией потока сообщений.

Так как каждый пользователь должен иметь поток сообщений, мы естественным образом пришли к feed методу в модели User. В конце концов, мы протестируем, что поток сообщений возвращает микросообщения пользователей, за которыми следит текущий пользователь, но пока мы просто протестируем, что feed метод включает в себя микросообщения текущего пользователя, но исключает сообщения других пользователей. Мы можем выразить эти требования в коде Листинга 10.35.

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:microposts) }
  it { should respond_to(:feed) }
  .
  .
  .
  describe "micropost associations" do

    before { @user.save }
    let!(:older_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
    end
    let!(:newer_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
    end
    .
    .
    .
    describe "status" do
      let(:unfollowed_post) do
        FactoryGirl.create(:micropost, user: FactoryGirl.create(:user))
      end

      its(:feed) { should include(newer_micropost) }
      its(:feed) { should include(older_micropost) }
      its(:feed) { should_not include(unfollowed_post) }
    end
  end
end
Листинг 10.35. Тесты для предварительной реализацией потока сообщений. spec/models/user_spec.rb

Эти тесты вводят (через булеву конвенцию RSpec) метод массива include?, который просто проверяет что массив включает данный элемент:9Изучение методов, таких как include? является одной из причин, по которым, как отмечалось в Разделе 1.1.1, я рекомендую читать книги о чистом Ruby после окончания этого учебника.

$ rails console
>> a = [1, "foo", :bar]
>> a.include?("foo")
=> true
>> a.include?(:bar)
=> true
>> a.include?("baz")
=> false

Этот пример показывает насколько гибкой является булевая конвенция RSpec; даже несмотря на то, что include уже является ключевым словом в Ruby (используется для включения модуля, как это можно увидеть в, например, Листинге 8.14), в этом контексте RSpec правильно угадывает что мы хотим протестировать включение в массив.

Мы можем организовать feed соответствующих микросообщений, выбрав все микросообщения с user_id равным id текущего пользователя, чего мы можем достигнуть используя where метод на модели Micropost, как это показано в Листинге 10.36.10См. Rails Guide Active Record Query Interface для того, чтобы узнать больше о where.

class User < ActiveRecord::Base
  .
  .
  .
  def feed
    # Это предварительное решение. См. полную реализацию в "Following users".
    Micropost.where("user_id = ?", id)
  end
  .
  .
  .
end
Листинг 10.36. Предварительная реализация потока микросообщений. app/models/user.rb

Знак вопроса в

Micropost.where("user_id = ?", id)

гарантирует, что id корректно маскирован прежде чем быть включенным в лежащий в его основе SQL запрос, что позволит избежать серьезной дыры в безопасности называемой SQL инъекция. Атрибут id в данном случае просто целое число (т.e., self.id, уникальный ID пользователя), так что в этом случае опасности нет, но постоянное маскирование переменных, вводимых в SQL выражение является хорошей привычкой.

Внимательные читатели могли отметить в этой точке, что код в Листинге 10.36 по сути эквивалентен записи

def feed
  microposts
end

Мы использовали код Листинга 10.36 вместо нее так как он генерализует гораздо более естественным образом полноценный поток микросообщений необходимый в Главе 11.

Для того чтобы протестировать отображение потока сообщений, мы вначале должны создать пару сообщений, а затем проверить что на странице присутствует элемент списка (li) для каждого из них (Листинг 10.37).

require 'spec_helper'

describe "Static pages" do

  subject { page }

  describe "Home page" do
    .
    .
    .
    describe "for signed-in users" do
      let(:user) { FactoryGirl.create(:user) }
      before do
        FactoryGirl.create(:micropost, user: user, content: "Lorem ipsum")
        FactoryGirl.create(:micropost, user: user, content: "Dolor sit amet")
        sign_in user
        visit root_path
      end

      it "should render the user's feed" do
        user.feed.each do |item|
          expect(page).to have_selector("li##{item.id}", text: item.content)
        end
      end
    end
  end
  .
  .
  .
end
Листинг 10.37. Тест рендеринга потока сообщений на странице Home. spec/requests/static_pages_spec.rb

Листинг 10.37 предполагает что каждый элемент потока сообщений имеет уникальный CSS id, так что

expect(page).to have_selector("li##{item.id}", text: item.content)

генерирует совпадение для каждого из них. (Обратите внимание что первый # в li##{item.id} является синтаксисом Capybara для CSS id, тогда как второй # является началом Рубишной интерполяции строк #{}.)

Для того чтобы использовать поток микросообщений в примере приложения, мы добавим переменную экземпляра @feed_items для (пагинированного) потока сообщений текущего пользователя как в Листинге 10.38, а затем добавим партиал потока сообщений (Листинг 10.39) к Home странице (Листинг 10.41). (Добавление тестов пагинаци остается в качестве упражнения; см. Раздел 10.5.)

class StaticPagesController < ApplicationController

  def home
    if signed_in?
      @micropost  = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end
  .
  .
  .
end
Листинг 10.38. Добавление переменной экземпляра потока сообщений к home действию. app/controllers/static_pages_controller.rb
<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render partial: 'shared/feed_item', collection: @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>
Листинг 10.39. Партиал потока сообщений. app/views/shared/_feed.html.erb

Партиал потока сообщений перекладывает рендеринг элемента потока сообщений на партиал элемента потока сообщений с помощью кода

<%= render partial: 'shared/feed_item', collection: @feed_items %<

Здесь мы передаем параметр :collection с элементами потока сообщений, что заставляет render использовать данный партиал (’feed_item’ в данном случае) для рендеринга каждого элемента в коллекции. (Мы опустили параметр :partial в предыдущих рендерингах, используя запись, например, render ’shared/micropost’, но с :collection параметром этот синтаксис не работает.) Сам партиал элемента потока сообщений представлен в Листинге 10.40.

<li id="<%= feed_item.id %>">
  <%= link_to gravatar_for(feed_item.user), feed_item.user %>
  <span class="user">
    <%= link_to feed_item.user.name, feed_item.user %>
  </span>
  <span class="content"><%= feed_item.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(feed_item.created_at) %> ago.
  </span>
</li>
Листинг 10.40. Партиал для отдельно взятого элемента потока сообщений. app/views/shared/_feed_item.html.erb

Листинг 10.40 также добавляет CSS id для каждого элемента потока сообщений с помощью

<li id="<%= feed_item.id %>">

как того требует тест в Листинге 10.37.

Затем мы можем добавить поток сообщений на Home страницу посредством рендеринга партиала потока сообщений как обычно (Листинг 10.41). В результате поток сообщений отображается на Home странице, как и требовалось ( рис. 10.14).

<% if signed_in? %>
  <div class="row">
    .
    .
    .
    <div class="span8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
<% else %>
  .
  .
  .
<% end %>
Листинг 10.41. Добавление потока микросообщений к Home странице. app/views/static_pages/home.html.erb
Home страница (/) с предварительной реализацией потока сообщений.

Рис. 10.14. Home страница (/) с предварительной реализацией потока сообщений.

На данный момент, создание новых микросообщений работает как надо, что показано на рис. 10.15. Однако есть одна тонкость: при неудачной отправке микросообщения Home страница ожидает переменную экземпляра @feed_items, таким образом провальная отправка в настоящее время не работает (что вы можете проверить запустив ваш набор тестов). Самым простым решением будет полное подавление потока сообщений через присвоение ему пустого массива, как это показано в Листинге 10.42.11К сожалению, возвращение пагинированного потока сообщений не работает в данном случае. Реализуйте это и кликните по пагинационной ссылке чтобы увидеть почему.

Home страница после создания нового микросообщения.

Рис. 10.15. Home страница после создания нового микросообщения.
class MicropostsController < ApplicationController
  .
  .
  .
  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @feed_items = []
      render 'static_pages/home'
    end
  end
  .
  .
  .
end
Листинг 10.42. Добавление (пустой) @feed_items переменной экземпляра к create действию. app/controllers/microposts_controller.rb

В этой точке предварительная реализация потока микросообщений должна работать, а набор тестов должен проходить:

$ bundle exec rspec spec/
< Лекция 9 || Лекция 10: 123456 || Лекция 11 >
Вадим Обозин
Вадим Обозин

Здравствуйте, записался на курс. При этом ставил галочку на "обучаться с тьютором". На email пришло письмо, о том, что записался на самостоятельное изучение курса. Как выбрать тьютора?

Акбар Ахвердов
Акбар Ахвердов
Россия, г. Москва
Артём Зайцев
Артём Зайцев
Украина, ДНР