Разработка приложений на Ruby on Rails

Слежение за сообщениями пользователей

Разбить на страницы
Показывать лекцию целиком

В этой главе мы завершим ядро примера приложения, добавив социальный слой, что позволит пользователям читать (и не читать) сообщения других пользователей, в результате чего на главной странице каждого пользователя будет отображаться список микросообщений тех пользователей, сообщения которых он читает. Мы также сделаем представления для отображения читающих и читаемых пользователей. Мы узнаем, как смоделировать взаимоотношения между пользователями в Разделе 11.1, а затем сделаем веб-интерфейс в Разделе 11.2 (включая введение в Ajax). Наконец, мы закончим, разработав полнофункциональный поток сообщений в Разделе 11.3.

Эта последняя глава содержит несколько из наиболее сложных материалов учебника, в том числе, сложные модели данных и несколько Ruby / SQL хитростей для создания потока сообщений. С помощью этих примеров вы увидите как Rails может обрабатывать даже весьма сложные модели данных, что должно вам пригодиться, так как вы двигаетесь к разработке собственных приложений с их собственными требованиями. Чтобы помочь с переходом от учебника к самостоятельной разработке, Раздел 11.4 содержит рекомендуемые расширения к ядру примера приложения, а также ссылки на более продвинутые ресурсы.

Как обычно, Git пользователи должны создать новую тему ветки:

$ git checkout -b following-users

Так как материал этой главы особенно сложен, прежде чем писать код, мы улучим момент и сделаем небольшой обзор интерфейса. Как и в предыдущих главах, на этом раннем этапе мы будем представлять страницы используя наброски.Фотографии для набросков взяты с http://www.flickr.com/photos/john_lustig/2518452221/ и http://www.flickr.com/photos/30775272@N05/2884963755/ Полная последовательность страниц работает следующим образом: пользователь, (John Calvin) начинает на странице своего профиля (рис 11.1) и переходит на страницу со списком пользователей (рис 11.2) для того, чтобы выбрать пользователя, сообщения которого он будет читать. Calvin переходит на страницу профиля выбранного пользователя, Thomas-а Hobbes-а (рис 11.3), кликает по кнопке "Follow", чтобы читать сообщения этого пользователя. Это изменяет кнопку "Follow" на "Unfollow", и увеличивает количество "followers" товарища Hobbes-а на единицу (рис 11.4). Вернувшись на свою главную страницу, Calvin теперь видит увеличившееся количество "following" и обнаруживает микросообщения Hobbes-а в своем потоке сообщений (рис 11.5). Остальная часть этой главы посвящена реализации этой последовательности.

(рис 11.1) Профиль текущего пользователя. (рис 11.2) Поиск пользователя для чтения его сообщений. (рис 11.3) Профиль другого пользователя с “Follow” кнопкой. (рис 11.4) Профиля с “Unfollow” кнопкой и увеличившимся количеством читателей. (рис 11.5) Главная страница с лентой сообщений и увеличившимся количеством читаемых пользователей.

Модель Relationship

Наш первый шаг в реализации слежения за сообщениями пользователей, заключается в построении модели данных, которая не так проста, как кажется. Naively, кажется, что has_many отношение должно сработать: пользователь has_many (имеет_много) читаемых и has_many (имеет_много) читателей. Как мы увидим, в этом подходе есть проблема, и мы узнаем как ее исправить используя has_many through. Вполне вероятно, что многие идеи этого раздела окажутся непонятыми с первого раза, и может потребоваться некоторое время для осознания довольно сложной модели данных. Если вы обнаружите что запутались, попробуйте пройти главу до конца, а затем прочитать этот раздел еще раз, чтобы прояснить для себя некоторые вещи.

Проблема с моделью данных (и ее решение)

В качестве первого шага на пути построения модели данных для слежения за сообщениями пользователей, давайте рассмотрим следующий типичный случай. Возьмем, в качестве примера, пользователя, который следит за сообщениями второго пользователя: мы могли бы сказать, что, например, Кальвин читает сообщения Гоббса, и Гоббс читается Кальвином, таким образом, Кальвин является читателем, а Гоббс является читаемым. При использовании дефолтной Rails’ плюрализации, множество таких читаемых пользователей называлось бы followers, и Первое издание этой книги использовало терминологию user.following, в которой даже я иногда путался. Благодарю читателя Cosmo Lee за то что он убедил меня изменить терминологию и за сформулированные советы о том как сделать это более понятным. (Однако не последовал его совету в точности, так что если вы все еще путаетесь - это не его вина.)

Это предполагает моделирование читаемых пользователей как на рис 11.6, с Для простоты, рис 11.6 подавляет id столбец таблицы following. Кроме того, так как каждая строка является пользователем, мы должны были бы включить другие атрибуты пользователя, включая имя, пароль и т.д.

(рис 11.6) Наивная реализация слежения за сообщениями пользователя.

Проблема модели данных из рис 11.6 в том, что она ужасно избыточна: каждая строка содержит не только id каждого читаемого пользователя, но и всю остальную информацию, уже содержащуюся в таблице users. Еще хуже то, что для моделирования читателей пользователя нам потребуется отдельная followers таблица. Наконец, эта модель данных кошмарно неудобна в эксплуатации, так как каждый раз при изменении пользователем (скажем) своего имени, нам пришлось бы обновлять запись пользователя не только в users таблице, но также каждую строку, содержащую этого пользователя в обоих followed_users и followers таблицах.

Проблема здесь в том, что нам не хватает лежащей в основе абстракции. Один из способов найти правильную абстракцию, это рассмотреть, как мы могли бы реализовать чтение сообщений пользователя в веб-приложении. Вспомним из Раздела 7.1.2, что REST архитектура включает в себя ресурсы которые создаются и уничтожаются. Это приводит нас к двум вопросам: Что создается, когда пользователь начинает читать сообщения другого пользователя? Что уничтожается, когда пользователь прекращает следить за сообщениями другого пользователя?

Поразмыслив, мы видим, что в этих случаях приложение должно создать либо разрушить взаимоотношение между двумя пользователями. Затем пользователь уже содержит большую часть реализации: поскольку каждый читаемый пользователь уникально идентифицирован посредством followed_id, мы можем преобразовать followed_users в таблицу relationships, опустив информацию о пользователе, и использовав followed_id для получения читаемых пользователей из users таблицы. Кроме того, приняв во внимание обратные взаимоотношения, мы могли бы использовать follower_id столбец для извлечения массива читателей пользователя.

Для того, чтобы создать массив пользователей followed_users, мы могли бы вытянуть массив атрибутов followed_id, а затем найти пользователя для каждого из них. Однако, как и следовало ожидать, в Rails есть более удобный способ для этой процедуры; соответствующая техника известна как has_many through. Как мы увидим в Разделе 11.1.4, Rails позволяет нам сказать, что пользователь следит за сообщениями многих пользователей через таблицу взаимоотношений, используя краткий код

has_many :followed_users, through: :relationships, source: :followed

Этот код автоматически заполняет .

(рис 11.7) Модель слежения пользователя за сообщениями через взаимоотношения.

Чтобы начать работу над реализацией, мы сначала генерируем модель Relationship следующим образом:

$ rails generate model Relationship follower_id:integer followed_id:integer

Возможно при этом была сгенерирована фабрика Relationship которую вам следует удалить:

$ rm -f spec/factories/relationship.rb

Так как мы будем искать взаимоотношения по follower_id и по followed_id, мы должны добавить индекс на каждой колонке для повышения эффективности поиска, как показано в Листинге 11.1.

class CreateRelationships < ActiveRecord::Migration
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

Листинге 11.1 включает также составной индекс, который обеспечивает уникальность пар (follower_id, followed_id), так что пользователь не может следить за сообщениями другого пользователя более одного раза:

add_index :relationships, [:follower_id, :followed_id], unique: true

(Сравните с индексом уникальности email из Листинга 6.9.) Как мы увидим в Разделе 11.1.4, наш пользовательский интерфейс не позволит этому случиться, но добавление индекса уникальности позволит избежать ошибки в случае, если пользователь попытается дублировать взаимотношения любым другим способом (используя, например, инструмент командной строки, такой как curl). Мы могли бы также добавить валидацию уникальности к модели Relationship, но, так как дублирование взаимоотношений является ошибкой всегда, для наших целей вполне достаточно индекса уникальности.

Для создания таблицы relationships, мы мигрируем базу данных и подготавливаем тестовую бд, как обычно:

$ bundle exec rake db:migrate
$ bundle exec rake test:prepare

Результирующая модель данных Relationship показана на рис 11.8.

(рис 11.8)

Ассоциации пользователь/взаимоотношение

Прежде чем приступить к реализации читателей и читаемых, нам вначале необходимо установить ассоциацию между пользователями и взаимоотношениями. Пользователь has_many (имеет_много) взаимоотношений, и, так как взаимоотношения включают двух пользователей — взаимоотношение belongs_to (принадлежит_к) читающим и читаемым пользователям.

Как и с микросообщениями в Разделе 10.1.3, мы будем создавать новые взаимоотношения используя ассоциацию, с помощью такого кода

user.relationships.build(followed_id: ...)

Мы начнем с базовых тестов валидации показанных в Листинге 11.2.

require 'spec_helper'

describe Relationship do

  let(:follower) { FactoryGirl.create(:user) }
  let(:followed) { FactoryGirl.create(:user) }
  let(:relationship) { follower.relationships.build(followed_id: followed.id) }

  subject { relationship }

  it { should be_valid }
end

Обратите внимание, что, в отличие от тестов для моделей User и Micropost, которые использовали Более подробно об этом см. when to use let at Stack Overflow. но я считаю let более чистым решением, нежели переменные экземпляра. Ранее мы применяли переменные экземпляра из-за того что нам важно было как можно раньше ввести переменные экземпляра, а также потому что let является немного более продвинутой техникой.

Мы также должны протестировать атрибут relationships модели User как это показано в Листинге 11.3.

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:feed) }
  it { should respond_to(:relationships) }
  .
  .
  .
end

В этой точке вы, возможно, ожидаете код приложения как в Разделе 10.1.3 - и он действительно похож, но есть одно важное отличие: в случае с моделью Micropost мы могли сказать

class Micropost < ActiveRecord::Base
  belongs_to :user
  .
  .
  .
end

и

class User < ActiveRecord::Base
  has_many :microposts
  .
  .
  .
end

поскольку у таблицы Если вы заметили что followed_id также идентифицирует пользователя, и обеспокоены ассиметричным обращением с читателями и читаемыми, вы готовы к любым неожиданностям. Мы займемся этим вопросом в Разделе 11.1.5.

class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :relationships, foreign_key: "follower_id", dependent: :destroy
  .
  .
  .
end

(Поскольку уничтожение пользователя должно также уничтожить его взаимоотношения мы пошли еще дальше и добавили dependent: :destroy к ассоциации; написание теста на это останется в качестве упражнения (Section 11.5).)

Как и у модели Micropost, у Relationship модели есть belongs_to взаимоотношения с пользователями; в данном случае, объект взаимоотношение принадлежит к обоим follower и followed пользователям, что мы и тестируем в Листинге 11.5.

describe Relationship do
  .
  .
  .
  describe "follower methods" do
    it { should respond_to(:follower) }
    it { should respond_to(:followed) }
    its(:follower) { should eq follower }
    its(:followed) { should eq followed }
  end
end

Чтобы написать код приложения, мы определяем belongs_to взаимоотношения как обычно. Rails выводит названия внешних ключей из соответствующих символов (т.e., follower_id из :follower, и followed_id из :followed), но, так как нет ни Followed ни Follower моделей, мы должны снабдить их именем класса User. Результат показан в Листинге 11.6. Обратите внимание, что, в отличие от дефолтно сгенерированной модели Relationship, в данном случае доступным является только followed_id.

class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

Ассоциация followed на самом деле не потребуется до Раздела 11.1.5, но параллельность структуры читатели/читаемые лучше видна при одновременной реализации.

В этой точке тесты из Листинга 11.2 и Листинга 11.3 должны пройти.

$ bundle exec rspec spec/

Валидации

Прежде чем двигаться дальше, мы добавим пару валидаций модели Relationship для комплектности. Тесты (Листинге 11.7) и код приложения (Листинг 11.8) просты.

describe Relationship do
  .
  .
  .
  describe "when followed id is not present" do
    before { relationship.followed_id = nil }
    it { should_not be_valid }
  end

  describe "when follower id is not present" do
    before { relationship.follower_id = nil }
    it { should_not be_valid }
  end
end
class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end

Читаемые пользователи

Теперь мы переходим к сердцу ассоциаций Relationship: followed_users и followers. Мы начнем с followed_users, как показано в Листинге 11.9.

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:relationships) }
  it { should respond_to(:followed_users) }
  .
  .
  .
end

Реализация впервые использует . По умолчанию, в ассоциации has_many through Rails ищет внешний ключ, соответствующий ассоциации в единственном числе; другими словами, код

has_many :followeds, through: :relationships

будет составлять массив, используя followed_id в таблице relationships. Но, как отмечалось в Разделе 11.1.1, user.followeds это довольно неуклюже; гораздо более естественным будет использование "followed users" в качестве множественного числа для "followed", и написание user.followed_users для массива читаемых пользователей. Естественно, Rails позволяет переопределить умолчание, в данном случае с помощью :source параметра (Листинг 11.10), который явно говорит Rails, что источником массива followed_users является множество followed ids.

class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :relationships, foreign_key: "follower_id", dependent: :destroy
  has_many :followed_users, through: :relationships, source: :followed
  .
  .
  .
end

Для того, чтобы создавать взаимоотношение с читаемым (пользователем), мы введем служебный метод Если у вас есть большой опыт моделирования конкретной предметной области, вы зачастую можете предугадать такие вспомогательные методы, и даже если нет, вы часто обнаруживаете себя за их написанием с целью почистить тесты. Однако в данном случае нормально если вы не угадали их. Разработка програмного обеспечения это обычно итеративный процесс — вы пишете код до тех пор пока он не начинает становиться уродливым, а затем вы рефакторите его — но, для краткости, изложение в учебнике немного сглажено. Тесты в Листинге 11.11 показывают как мы планируем использовать эти методы на практике.

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:followed_users) }
  it { should respond_to(:following?) }
  it { should respond_to(:follow!) }
  .
  .
  .
  describe "following" do
    let(:other_user) { FactoryGirl.create(:user) }
    before do
      @user.save
      @user.follow!(other_user)
    end

    it { should be_following(other_user) }
    its(:followed_users) { should include(other_user) }
  end
end

В коде приложения, метод following? принимает пользователя, названного other_user и проверяет, существует ли он в базе данных; метод follow! вызывает create! через relationships ассоциацию для создания взаимоотношения с читаемым. Результаты представлены в Листинге 11.12.

class User < ActiveRecord::Base
  .
  .
  .
  def feed
    .
    .
    .
  end

  def following?(other_user)
    relationships.find_by(followed_id: other_user.id)
  end

  def follow!(other_user)
    relationships.create!(followed_id: other_user.id)
  end
  .
  .
  .
end

Отметим, что в Листинге 11.12 мы опустили самого пользователя, написав просто

relationships.create!(...)

вместо эквивалентного кода

self.relationships.create!(...)

Явное включение или невключение self в данном случае дело вкуса.

Конечно, пользователи должны иметь возможность прекратить слежение за сообщениями других пользователей, что приводит нас к немного предсказуемому методу unfollow!, как показано в Листинге 11.13.

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:follow!) }
  it { should respond_to(:unfollow!) }
  .
  .
  .
  describe "following" do
    .
    .
    .
    describe "and unfollowing" do
      before { @user.unfollow!(other_user) }

      it { should_not be_following(other_user) }
      its(:followed_users) { should_not include(other_user) }
    end
  end
end

Код для unfollow! прост: нужно просто найти взаимоотношение по followed id и уничтожить его (Листинг 11.14).

class User < ActiveRecord::Base
  .
  .
  .
  def following?(other_user)
    relationships.find_by(followed_id: other_user.id)
  end

  def follow!(other_user)
    relationships.create!(followed_id: other_user.id)
  end

  def unfollow!(other_user)
    relationships.find_by(followed_id: other_user.id).destroy!
  end
  .
  .
  .
end

Читатели пользователя

Последней частью пазла взаимоотношений является метод что все сведения, необходимые для извлечения массива читателей уже присутствуют в таблице ), то мы бы с легкостью реализовали user.followers.

(рис 11.9) Модель данных для читателей пользователя, использующая реверсированную модель Relationship.

Начнем с тестов, веря, что магия Rails выручит нас (когда дело дойдет до реализации) (Листинг 11.15).

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:relationships) }
  it { should respond_to(:followed_users) }
  it { should respond_to(:reverse_relationships) }
  it { should respond_to(:followers) }
  .
  .
  .

  describe "following" do
    .
    .
    .
    it { should be_following(other_user) }
    its(:followed_users) { should include(other_user) }

    describe "followed user" do
      subject { other_user }
      its(:followers) { should include(@user) }
    end
    .
    .
    .
  end
end

Обратите внимание на то, как мы изменили субъект с помощью метода subject, замена @user на other_user, позволяет нам протестировать взаимоотношение с читателями естесственным образом:

subject { other_user }
its(:followers) { should include(@user) }

Как вы наверное подозреваете, мы не будем создавать полную таблицу в базе данных только для того чтобы просто произвести реверс взаимоотношений. Вместо этого мы воспользуемся базовой симметрией между читаемыми и читателями для симуляции таблицы reverse_relationships, передав followed_id в качестве внешнего ключа. Иными словами, там где ассоциация relationships использует внешний ключ follower_id,

has_many :relationships, foreign_key: "follower_id"

ассоциация reverse_relationships использует followed_id:

has_many :reverse_relationships, foreign_key: "followed_id"

Ассоциация followers затем строится через реверсированные взаимоотношения, как показано в Листинге 11.16.

class User < ActiveRecord::Base
  .
  .
  .
  has_many :reverse_relationships, foreign_key: "followed_id",
                                   class_name:  "Relationship",
                                   dependent:   :destroy
  has_many :followers, through: :reverse_relationships, source: :follower
  .
  .
  .
end

(Как и с Листингом 11.4, тест для dependent :destroy остается в качестве упражнения (Раздел 11.5).) Обратите внимание, что мы должны включить имя класса для этой ассоциации, т.e.,

has_many :reverse_relationships, foreign_key: "followed_id",
                                 class_name: "Relationship"

потому что иначе Rails будет искать несуществующий класс ReverseRelationship.

Стоит также отметить, что мы могли бы в этом случае пропустить :source, используя просто

has_many :followers, through: :reverse_relationships

поскольку Rails будет автоматически искать внешний ключ follower_id в данном случае. Я сохранил ключ :source для того чтобы подчеркнуть параллельность со структурой ассоциации has_many :followed_users, но вы можете пропустить его.

С кодом в Листинге 11.16, ассоциации читаемые/читатели завершены, и все тесты должны пройти:

$ bundle exec rspec spec/

Этот раздел предъявил довольно высокие требования к вашим навыкам моделирования данных, и это нормально, если для его усвоения потребуется некоторое время. Фактически, одним из самых лучших способов понять ассоциации является их использование в веб интерфейсе, как мы увидим в следующем разделе.

Веб-интерфейс для читаемых пользователей

Во введении к этой главе, мы сделали предварительный обзор страниц, необходимых для слежения за сообщениями пользователей. В этом разделе мы реализуем базовый интерфейс и читать/не читать функциональность, показанные в тех макетах. Мы также сделаем отдельные страницы для демонстрации массивов читателей и читаемых. В Разделе 11.3, мы завершим наш пример приложения, добавив поток сообщений пользователя.

Образцы данных

Как и в предыдущих главах, нам удобно будет использовать Rake задачу для заполнения базы данных образцами взаимоотношений. Это позволит нам заняться в первую очередь внешним видом страниц, временно отложив серверную часть функциональности.

Когда мы последний раз видели заполнитель образцов данных в Листинге 11.20, он был немного суматошным. Поэтому мы начнем с определения отдельных методов для создания пользователей и микросообщений, а затем добавим образцы данных взаимоотношений используя новый метод make_relationships. Результаты показаны в Листинге 11.17.

namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    make_users
    make_microposts
    make_relationships
  end
end

def make_users
  admin = User.create!(name:     "Example User",
                       email:    "example@railstutorial.org",
                       password: "foobar",
                       password_confirmation: "foobar",
                       admin: true)
  99.times do |n|
    name  = Faker::Name.name
    email = "example-#{n+1}@railstutorial.org"
    password  = "password"
    User.create!(name:     name,
                 email:    email,
                 password: password,
                 password_confirmation: password)
  end
end

def make_microposts
  users = User.all(limit: 6)
  50.times do
    content = Faker::Lorem.sentence(5)
    users.each { |user| user.microposts.create!(content: content) }
  end
end

def make_relationships
  users = User.all
  user  = users.first
  followed_users = users[2..50]
  followers      = users[3..40]
  followed_users.each { |followed| user.follow!(followed) }
  followers.each      { |follower| follower.follow!(user) }
end

Здесь образцы взаимоотношений создаются с помощью кода

def make_relationships
  users = User.all
  user  = users.first
  followed_users = users[2..50]
  followers      = users[3..40]
  followed_users.each { |followed| user.follow!(followed) }
  followers.each      { |follower| follower.follow!(user) }
end

Мы несколько произвольно организовали слежение первого пользователя за сообщениями пользователей с 3 по 51, а затем принудили пользователей с 4 по 41 читать сообщения первого пользователя. Полученных в результате взаимоотношений будет вполне достаточно для разработки интерфейса приложения.

Чтобы выполнить код Листинга 11.17, заполним базу данных как обычно:

$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake test:prepare

Статистика и форма для слежения за сообщениями пользователя

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

Как было отмечено в Разделе 11.1.1, слово "following" является двусмысленным при использовании в качестве атрибута (где user.following могло бы означать или читаемых пользователей или читателей пользователя), но оно вполне подходит в качестве метки, как в "50 following". Кроме того, это метка которой пользуется сам Twitter, использование принятое в набросках начиная с рис 11.1 и показанное крупным планом на рис 11.10.

(рис 11.10) Набросок партиала статистики.

Статистика на рис 11.10 содержит и количество читаемых пользователей, и количество его читателей, каждое из чисел должно быть ссылкой на соответствующую специальную страницу для их отображения. В Главе 5, мы временно заглушали подобные ссылки знаком ’#’, но это было до того, как мы набрались опыта с маршрутами. Сейчас, несмотря на то, что мы отложили сами страницы до Раздела 11.2.3, мы сделаем маршруты сейчас, как показано в Листинге 11.18. Этот код использует метод :member внутри блока resources, с которым мы ранее не были знакомы, но посмотрим, сможете ли вы угадать, что он делает. (Примечание: код в Листинге 11.18 должен заменить resources :users.)

SampleApp::Application.routes.draw do
  resources :users do
    member do
      get :following, :followers
    end
  end
  .
  .
  .
end

Вы возможно догадываетесь, что URL для читаемых и читающих пользователей будут выглядеть как /users/1/following и /users/1/followers, и это именно то, что делает код в Листинге 11.18. Поскольку обе страницы будут отображать данные, мы используем get для того чтобы организовать ответ URL на запросы GET (что требуется конвенцией REST для подобных страниц), и метод member означает, что маршруты отвечают на URL, содержащие id пользователя. (Другой возможный метод, collection, работает без id, так что

resources :users do
  collection do
    get :tigers
  end
end

будет отвечать на URL /users/tigers (presumably to display all the tigers in our application). — предположительно для отображения всех тигров нашего приложения. Узнать больше об этой опции можно из Ruby on Rails по-русски "Роутинг в Rails". Таблица маршрутов, сгенерированных Листинге 11.18 представлена в Таблице 11.1; обратите внимание на именнованные маршруты для страниц с читателями и читаемыми, которые мы вскоре будем использовать. Неудачное гибридное применение в маршруте "following" обусловлено нашим решением использовать недвусмысленную терминологию "followed users" наряду с применением "following" взятым у Twitter. Поскольку предыдущий привел бы нас к маршрутам вида followed_users_user_path, что звучит довольно странно, мы выбрали последний в контексте Таблицы 11.1, что привело к following_user_path.

RESTful маршруты предоставленные кастомными правилами в ресурсе из Листинга 11.18.
HTTP request URL Действие Именованный маршрут
GET /users/1/following following following_user_path(1)
GET /users/1/followers followers followers_user_path(1)

Определив маршруты мы готовы сделать тесты партиала статистики. (Мы могли бы начать с тестов, но именованные маршруты было бы трудно объяснить без обновленного файла маршрутов.) Партиал появится на странице профиля и на странице Home;

require 'spec_helper'

describe "Static pages" do
  .
  .
  .
  describe "Home page" do
    .
    .
    .
    describe "for signed-in users" do
      let(:user) { FactoryGirl.create(:user) }
      before do
        FactoryGirl.create(:micropost, user: user, content: "Lorem")
        FactoryGirl.create(:micropost, user: user, content: "Ipsum")
        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

      describe "follower/following counts" do
        let(:other_user) { FactoryGirl.create(:user) }
        before do
          other_user.follow!(user)
          visit root_path
        end

        it { should have_link("0 following", href: following_user_path(user)) }
        it { should have_link("1 followers", href: followers_user_path(user)) }
      end
    end
  end
  .
  .
  .
end

Ядром этих тестов является предположение, что количество читателей и читаемых представлено на странице совместно с правильными URL:

it { should have_link("0 following", href: following_user_path(user)) }
it { should have_link("1 followers", href: followers_user_path(user)) }

Здесь мы использовали именованные маршруты, показанные в Таблице 11.1 для проверки того, что ссылки имеют правильные адреса. Также обратите внимание на то, что в данном случае слово "followers" работает как метка, так что мы сохраним его во множественном числе, даже если будет только один читатель.

Код приложения для партиала статистики это просто несколько ссылок внутри div, как показано в Листинге 11.20.

<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.followed_users.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>

Поскольку мы включим статистику и на профиле пользователя и на Home странице, первая строка Листинга 11.20 выбирает правильное с помощью

<% @user ||= current_user %>

Как обсуждалось в Блоке 8.2, если @user не является nil, то ничего не происходит (как и на странице профиля), но когда он существует (как на странице Home), он назначает @user равным текущему пользователю.

Обратите внимание также на то, что количество читаемых/читателей подсчитывается через ассоциацию с помощью

@user.followed_users.count

и

@user.followers.count

Сравните это с подсчетом количества микросообщений из Листинга 11.17, где мы писали

@user.microposts.count

для подсчета микросообщений.

Одна последняя деталь которую стоит отметить - наличие CSS id у некоторых элементов, как в

<strong id="following" class="stat">
...
</strong>

Это сделано в угоду Ajax реализации из Раздела 11.2.5, которая получает доступ к элементам страницы используя их уникальные id.

С готовым партиалом включить статистику в Home страницу проще простого, как показано в Листинге 11.21. (Это также приводит к прохождению тестов из Листинга 11.19.)

<% if signed_in? %>
      .
      .
      .
      <section>
        <%= render 'shared/user_info' %>
      </section>
      <section>
        <%= render 'shared/stats' %>
      </section>
      <section>
        <%= render 'shared/micropost_form' %>
      </section>
      .
      .
      .
<% else %>
  .
  .
  .
<% end %>

Для того чтобы придать статистике стиль, мы добавим немного SCSS, как это показано в Листинге 11.22 (который содержит весь код таблиц стилей необходимый в этой главе). Результат представлен на рис 11.22" type="example">Листинге 11.22 (который содержит весь код таблиц стилей необходимый в этой главе). Результат представлен на . . . /* sidebar */ . . . .stats { overflow: auto; a { float: left; padding: 0 10px; border-left: 1px solid $grayLighter; color: gray; :first-child { padding-left: 0; border: 0; } :hover { text-decoration: none; color: $blue; } } strong { display: block; } } .user_avatars { overflow: auto; margin-top: 10px; .gravatar { margin: 1px 1px; } } . . . (рис 11.11) Home страница (/) со статистикой.

Мы подключим статистику к странице профиля через мгновение, но вначале давайте сделааем партиал для follow/unfollow кнопки, как показано в Листинге 11.22.

<% unless current_user?(@user) %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

Этот партиал ничего не делает кроме перекладывания реальной работы на follow и unfollow партиалы, которым нужен новый файл маршрутов чьи правила для ресурса Relationships, следуют примеру ресурса Microposts (Листинг 11.22), как показано в Листинге 11.24.

SampleApp::Application.routes.draw do
  .
  .
  .
  resources :sessions,      only: [:new, :create, :destroy]
  resources :microposts,    only: [:create, :destroy]
  resources :relationships, only: [:create, :destroy]
  .
  .
  .
end

Сами партиалы follow/unfollow показаны в Листинге 11.25 и Листинге 11.26.

<%= form_for(current_user.relationships.build(followed_id: @user.id)) do |f| %>
  <div><%= f.hidden_field :followed_id %></div>
  <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
<% end %>
<%= form_for(current_user.relationships.find_by(followed_id: @user),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn btn-large" %>
<% end %>

Обе эти формы используют form_for для манипуляций с объектом модели Relationship; основное отличие между ними заключается в том, что Листинг 11.25 строит новое взаимоотношение, тогда как Листинг 11.26 ищет существующее взаимоотношение. Естественно, первый отправляет POST запрос к контроллеру Relationships для create взаимоотношения, в то время как последний отправляет DELETE запрос для destroy взаимоотношения. (Мы напишем эти действия в Разделе 11.2.4.) Наконец, отметьте что форма follow/unfollow не содержит никакого контента кроме кнопки, но нам все еще необходимо отправить followed_id, чего мы можем добиться с помощью метода hidden_field из Листинга 11.25; который производит HTML вида

<input id="relationship_followed_id"
name="relationship[followed_id]"
type="hidden" value="3" />

Тег "hidden" input поместит соответствующую информацию на странице не отображая ее в браузере.

Теперь мы можем включить форму чтения и статистику на страницу профиля пользователя простым рендерингом партиалов, как показано в Листинге 11.27. Профили с follow и unfollow кнопками, соответственно, представлены на рис 11.27" type="example">Листинге 11.27. Профили с follow и unfollow кнопками, соответственно, представлены на рис 11.13.

<% provide(:title, @user.name) %>
<div class="row">
  <aside class="span4">
    <section>
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <section>
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="span8">
    <%= render 'follow_form' if signed_in? %>
    .
    .
    .
  </div>
</div>
(рис 11.12) Профиль пользователя с follow кнопкой (/users/2). (рис 11.13) Профиль пользователя с unfollow кнопкой (/users/6).

Мы вскоре сделаем эти кнопки рабочими, фактически, мы сделаем это двумя способами, стандартным способом (Раздел 11.2.4) и с помощью Ajax (Раздел 11.2.5), но вначале мы закончим HTML интерфейс, создав страницы для списков читающих и читаемых.

Страницы с читаемыми и читателями

Страницы для отображения читающих сообщения пользователя и читаемых им пользователей будут напоминать гибрид страницы профиля пользователя и страницы со списком пользователей (Раздел 9.3.1), с сайдбаром пользовательской информации (включая статистику слежения за сообщениями) и таблицу пользователей. Кроме того, мы включим сетку пользовательских профильных изображений-ссылок в сайдбаре. Набросок соответствующий этим требованиям представлен на рис 11.14 (читаемые) и рис 11.15 (читатели).

(рис 11.14) Набросок страницы со списком читаемых пользователей. (рис 11.15) Набросок страницы со списком читателей.

Нашим первым шагом будет получение рабочих ссылок following (читаемые) и followers (читатели). Мы будем следовать примеру Твиттера и обе страницы будут требовать входа пользователя, как было протестировано в Листинге 11.28. Для вошедших пользователей, страницы должны иметь ссылки на читаемых и читателей, соответственно, как протестировано в Листинге 11.29.

require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      let(:user) { FactoryGirl.create(:user) }

      describe "in the Users controller" do
        .
        .
        .
        describe "visiting the following page" do
          before { visit following_user_path(user) }
          it { should have_title('Sign in') }
        end

        describe "visiting the followers page" do
          before { visit followers_user_path(user) }
          it { should have_title('Sign in') }
        end
      end
      .
      .
      .
    end
    .
    .
    .
  end
  .
  .
  .
end
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "following/followers" do
    let(:user) { FactoryGirl.create(:user) }
    let(:other_user) { FactoryGirl.create(:user) }
    before { user.follow!(other_user) }

    describe "followed users" do
      before do
        sign_in user
        visit following_user_path(user)
      end

      it { should have_title(full_title('Following')) }
      it { should have_selector('h3', text: 'Following') }
      it { should have_link(other_user.name, href: user_path(other_user)) }
    end

    describe "followers" do
      before do
        sign_in other_user
        visit followers_user_path(other_user)
      end

      it { should have_title(full_title('Followers')) }
      it { should have_selector('h3', text: 'Followers') }
      it { should have_link(user.name, href: user_path(user)) }
    end
  end
end

Единственно сложная часть реализации это осуществление потребности добавления двух новых действий к контроллеру Users; основанных на маршрутах, определенных в Листинге 11.18, нам нужно назвать их following и followers. Каждому действию нужно установить заголовок, найти пользователя, вытянуть @user.followed_users или @user.followers (в пагинированной форме), а затем отренедерить страницу. Результат представлен в Листинге 11.30.

class UsersController < ApplicationController
  before_action :signed_in_user,
                only: [:index, :edit, :update, :destroy, :following, :followers]
  .
  .
  .
  def following
    @title = "Following"
    @user = User.find(params[:id])
    @users = @user.followed_users.paginate(page: params[:page])
    render 'show_follow'
  end

  def followers
    @title = "Followers"
    @user = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end

  private
  .
  .
  .
end

Отметим здесь, что оба действия делают явный вызов render, в данном случае делая рендеринг представления, названного show_follow, которое мы должны создать. Причина создания общего представления в том, что ERb является практически идентичным для обоих случаев, и Листинг 11.31 охватывает их.

<% provide(:title, @title) %>
<div class="row">
  <aside class="span4">
    <section>
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section>
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="span8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

С этим тесты должны пройти, а страницы должны выглядеть как на рис 11.16(читаемые) и рис 11.17(читатели).

(рис 11.16) Отображение пользователей, читаемых текущим пользователем. (рис 11.17) Отображение читателей текущего пользователя.

Стандартный способ реализации кнопки "читать" (follow)

Теперь, когда наши представления в порядке, пришло время получить рабочие follow/unfollow кнопки. Тесты для этих кнопок комбинируют множество техник тестирования о которых было рассказано в этом учебнике и представляют из себя хорошее упражнение в чтении кода. Изучайте Листинг 11.32 до тех пор пока не убедитесь что вы понимаете что и почему мы тестируем. (Листинг 11.32 содержит одно небольшое упущение в безопасности; посмотрим, сможете ли вы выявить его. Мы скоро расскажем о нем.) Особенно обратите внимание на использование метода have_xpath использующего продвинутую и мощную технику XPath для навигации по XML документам (включая HTML5). Вы можете узнать больше о XPath с помощью поискового запроса XPath syntax.

require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "profile page" do
    let(:user) { FactoryGirl.create(:user) }
    .
    .
    .
    describe "follow/unfollow buttons" do
      let(:other_user) { FactoryGirl.create(:user) }
      before { sign_in user }

      describe "following a user" do
        before { visit user_path(other_user) }

        it "should increment the followed user count" do
          expect do
            click_button "Follow"
          end.to change(user.followed_users, :count).by(1)
        end

        it "should increment the other user's followers count" do
          expect do
            click_button "Follow"
          end.to change(other_user.followers, :count).by(1)
        end

        describe "toggling the button" do
          before { click_button "Follow" }
          it { should have_xpath("//input[@value='Unfollow']") }
        end
      end

      describe "unfollowing a user" do
        before do
          user.follow!(other_user)
          visit user_path(other_user)
        end

        it "should decrement the followed user count" do
          expect do
            click_button "Unfollow"
          end.to change(user.followed_users, :count).by(-1)
        end

        it "should decrement the other user's followers count" do
          expect do
            click_button "Unfollow"
          end.to change(other_user.followers, :count).by(-1)
        end

        describe "toggling the button" do
          before { click_button "Unfollow" }
          it { should have_xpath("//input[@value='Follow']") }
        end
      end
    end
  end
  .
  .
  .
end

Листинг 11.32 тестирует кнопки кликая по ним и проверяя соответствующее поведение. Написание реализации подразумевает чуть более глубокое погружения в тему: following и unfollowing включает создание и уничтожение взаимоотношений, что означает необходимость определения create и destroy действий в контроллере Relationships (который нам еще нужно создать). Хотя кнопки появляются только для вошедших пользователей, что дает нам безопасность верхнего уровня, тесты в Листинге 11.32 упускают из виду безопасность на уровне контроллера, а именно: сами create и destroy должны быть доступны только для вошедших пользователей. (Это та самая уязвимость о которой мы говорили выше.) Листинг 11.33 выражает эти требования с помощью post и delete методов вызывающих эти действия напрямую.

require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      let(:user) { FactoryGirl.create(:user) }
      .
      .
      .
      describe "in the Relationships controller" do
        describe "submitting to the create action" do
          before { post relationships_path }
          specify { expect(response).to redirect_to(signin_path) }
        end

        describe "submitting to the destroy action" do
          before { delete relationship_path(1) }
          specify { expect(response).to redirect_to(signin_path) }
        end
      end
      .
      .
      .
    end
  end
end

Обратите внимание, что, для того чтобы избежать лишней работы по созданию практически бесполезного объекта Relationship, тест delete хардкодит id 1 в именованном маршруте:

before { delete relationship_path(1) }

Это работает из-за того что пользователь должен быть перенаправлен прежде чем приложение даже попытается обратиться к взаимоотношению с этим id.

Код контроллера, необходимый для прохождения этих тестов удивительно краток: мы просто вытягиваем читаемого или читающего пользователя, а затем читаем или не читаем его сообщения используя соответствующий служебный метод. Полная реализация представлена в Листинге 11.34.

class RelationshipsController < ApplicationController
  before_action :signed_in_user

  def create
    @user = User.find(params[:relationship][:followed_id])
    current_user.follow!(@user)
    redirect_to @user
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow!(@user)
    redirect_to @user
  end
end

В Листинге 11.34 мы можем видеть почему уязвимость отмеченная выше является незначительной: если невошедший пользователь попробовал бы обратиться к любому из действий напрямую (например, с помощью инструмента командной строки вроде curl), current_user был бы nil и в обоих случаях вторая строка действий вызвала бы исключение, что привело бы к ошибке, но не нанесло бы вреда приложению или его данным. Однако лучше на это не полагаться, так что мы предприняли дополнительный шаг и добавили дополнительный уровень безопасности.

С этим ядро функциональности follow/unfollow завершено, и любой пользователь может читать (или не читать) сообщения любого другого пользователя, что вам стоит проверить и в вашем браузере и запустив набор тестов:

$ bundle exec rspec spec/

Реализация кнопки "читать" (follow) с Ajax

Хотя наша реализация слежения за сообщениями пользователей является законченной и в своем нынешнем виде, нам осталось совсем немного подправить ее прежде чем заняться потоком сообщений. Вы могли заметить в Разделе 11.2.4 что оба create и destroy действия в контроллере Relationships просто перенаправляют обратно к исходному профилю. Другими словами, пользователь начинает на странице профиля, подписывается на сообщения пользователя и немедленно перенаправляется на исходную страницу. Резонный вопрос - почему пользователь вообще должен покидать эту страницу?

Именно эту проблему решает Ajax, который позволяет веб страницам отправлять асинхронные запросы на сервер не покидая страницы.Так как номинально это является акронимом asynchronous JavaScript and XML, Ajax иногда ошибочно пишут как "AJAX", хотя на протяжении всей оригинальной Ajax статьи используется написание "Ajax". Поскольку практика добавления Ajax в веб формы является довольно распространенной, Rails делает реализацию Ajax легкой. Действительно, обновление партиалов формы follow/unfollow тривиально: просто заменим

form_for

на

form_for ..., remote: true

и Rails автомагически будет использовать Ajax.Это работает только если JavaScript включен в браузере, но изящно деградирует, работая в точности как в Разделе 11.2.4 если JavaScript отключен. Обновленные партиалы представлены в Листинге 11.35 и Листинге 11.36.

<%= form_for(current_user.relationships.build(followed_id: @user.id),
             remote: true) do |f| %>
  <div><%= f.hidden_field :followed_id %></div>
  <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
<% end %>
<%= form_for(current_user.relationships.find_by(followed_id: @user),
             html: { method: :delete },
             remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn btn-large" %>
<% end %>

HTML сгенерированный этим ERb не особенно относится к делу, но вам может быть любопытно, так что взгляните:

<form action="/relationships/117" class="edit_relationship" data-remote="true"
      id="edit_relationship_117" method="post">
  .
  .
  .
</form>

Это устанавливает переменную data-remote="true" внутри тега формы, что говорит Rails о том, что форма будет обрабатываться JavaScript. Используя простое свойство HTML вместо вставки полного JavaScript кода (как в предыдущих версиях Rails), Rails 3 следует философии ненавязчивого JavaScript.

После обновления формы нам нужно уговорить контроллер Relationships отвечать на Ajax запросы. Тестирование Ajax является довольно сложным, и тщательное делание этого является большой темой со своими собственными правилами, но мы можем начать с кодом в Листинге 11.37. Это использует xhr метод (от "XmlHttpRequest") для выдачи Ajax запроса; сравните с get, post, patch и delete методами в предыдущих тестах. Затем мы проверяем что create и destroy действия делают правильные вещи когда вызываются Ajax запросом. (Для написания более основательного набора тестов для насыщенных Ajax-ом приложений, взгляните на Selenium и Watir.)

require 'spec_helper'

describe RelationshipsController do

  let(:user) { FactoryGirl.create(:user) }
  let(:other_user) { FactoryGirl.create(:user) }

  before { sign_in user, no_capybara: true }

  describe "creating a relationship with Ajax" do

    it "should increment the Relationship count" do
      expect do
        xhr :post, :create, relationship: { followed_id: other_user.id }
      end.to change(Relationship, :count).by(1)
    end

    it "should respond with success" do
      xhr :post, :create, relationship: { followed_id: other_user.id }
      expect(response).to be_success
    end
  end

  describe "destroying a relationship with Ajax" do

    before { user.follow!(other_user) }
    let(:relationship) { user.relationships.find_by(followed_id: other_user) }

    it "should decrement the Relationship count" do
      expect do
        xhr :delete, :destroy, id: relationship.id
      end.to change(Relationship, :count).by(-1)
    end

    it "should respond with success" do
      xhr :delete, :destroy, id: relationship.id
      expect(response).to be_success
    end
  end
end

Код в Листинге 11.37 это наш первый пример тестов контроллера, которыми я ранее интенсивно пользовался (например в предыдущем издании этого учебника), но которым я сейчас предпочитаю интеграционные тесты. Однако в данном случае, метод xhr (по непонятным мне причинам) не доступен в интеграционных тестах. Хотя мы впервые используем xhr, в этой точке учебника вы, вероятно, уже можете понять из контекста чем занимается этот код:

xhr :post, :create, relationship: { followed_id: other_user.id }

Мы видим что xhr принимает в качестве аргумента символ для соответствующего метода HTTP, символ для действия и хэш представляющий собой содержимое params в самом контроллере. Как и в предыдущих примерах, мы используем expect для того чтобы обернуть операцию в блок и протестировать увеличение или уменьшение соответсвующего количества.

Как следует из тестов, код приложения использует те же create и delete действия для ответа на Ajax запросы которые он выдает чтобы ответить на обычные POST и DELETE HTTP запросы. Все что нам нужно сделать это ответить на обычный HTTP запрос с переадресацией (как в Разделе 11.2.4) и ответить на Ajax запрос с JavaScript. Код контроллера представлен в Листинге 11.38. (См. в Разделе 11.5 пример показывающий более компактный способ выполнить то же самое.)

class RelationshipsController < ApplicationController
  before_action :signed_in_user

  def create
    @user = User.find(params[:relationship][:followed_id])
    current_user.follow!(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow!(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

Этот код использует respond_to чтобы принять соответствующее действие в зависимости от вида запроса. (Это respond_to никак не связано с respond_to используемым в примерах RSpec.) Синтаксис может запутать и важно понимать, что в

respond_to do |format|
  format.html { redirect_to @user }
  format.js
end	

выполняется только одна из строк (в зависимости от характера запроса).

В случае Ajax запроса, Rails автоматически вызывает JavaScript Embedded Ruby (.js.erb) файл с тем же именем что и действие, т.е., create.js.erb или destroy.js.erb. Как вы можете догадаться, эти файлы позволяют смешивать JavaScript и Embedded Ruby для выполнения действий на текущей странице. Именно эти файлы нам нужны для создания и редактирования страницы профиля пользователя после начала слежения за сообщениями пользователя или после его прекращения.

Внутри JS-ERb файла, Rails автоматически обеспечивает jQuery JavaScript хелперы для манипуляции страницей при помощи Document Object Model (DOM). Библиотека jQuery предоставляет большое количество методов для манипуляции DOM, но здесь нам понадобятся только два. Во первых мы должны знать о синтаксисе знака доллара, используемого для доступа к DOM элементу опираясь на его уникальный CSS id. Например, для манипуляции элементом follow_form, мы используем синтаксис

$("#follow_form")

(Вспомните из Листинг 11.23 что это div который обертывает форму, а не сама форма.) Этот синтаксис, вдохновленный CSS, использует символ # для указания CSS id. Как вы можете догадаться, jQuery, как и CSS, использует точку . для манипуляций с CSS классами.

Второй метод который нам потребуется это html, который обновляет HTML внутри соответствующего элемента содержимым своего аргумента. Например, чтобы полностью заменить follow form на строку "foobar", мы можем написать

$("#follow_form").html("foobar")

В отличие от простых JavaScript файлов, JS-ERb файлы позволяют также использовать Embedded Ruby, что мы применяем в create.js.erb файле для замены формы на партиал unfollow (это то, что должно быть видно после успешной подписки на сообщения пользователя) и обновления количества читаемых. Результат представлен в Листинг 11.39. Здесь используется функция escape_javascript, которая необходима для маскирования результатов при вставке HTML в файл JavaScript.

$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
$("#followers").html('<%= @user.followers.count %>')

Файл destroy.js.erb аналогичен (Листинг 11.40).

$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>")
$("#followers").html('<%= @user.followers.count %>')

Теперь вам следует перейти на страницу профиля пользователя и проверить, что вы можете следить и не следить за сообщениями пользователей без обновления страницы, а набор тестов должен позеленеть:

$ bundle exec rspec spec/

Использование Ajax в Rails является большой и стремительно развивающейся темой, и здесь мы лишь слегка коснулись ее, но (как и с остальными материалами в этом учебнике) наш подход дает вам хорошую основу для изучения более продвинутых ресурсов.

Поток сообщений

Мы подошли к кульминации нашего примера приложения: потоку сообщений. Соответствено, этот раздел содержит некоторые из самых продвинутых материалов в данном учебнике. Полноценный поток сообщений опирается на свой прототип из Раздела 10.3.3 и собирает массив микросообщений из микросообщений читаемых пользователей, совместно с собственными микросообщениями текущего пользователя. Чтобы совершить этот подвиг, нам понадобятся некоторые довольно продвинутые Rails, Ruby и даже SQL техники программирования.

Так как нам предстоит тяжелая работа, особенно важно понимать куда мы будем двигаться. Резюме окончательного потока сообщений пользователя, показанный на рис 11.5, вновь показан на рис 11.18.

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

Мотивация и стратегия

Основная идея потока сообщений проста. рис 11.19 показывает пример таблицы базы данных microposts и результирующий поток сообщений. Цель потока заключается в вытягивании микросообщений чей user id соответствует пользователям, сообщения которых читает текущий пользователь (и id самого текущего пользователя), как указано стрелками на схеме.

(рис 11.19) Поток сообщений для пользователя (id 1) читающего сообщения пользователей 2, 7, 8 и 10.

Поскольку нам необходим способ найти все микросообщения пользователей, за которыми следит данный пользователь, мы запланируем реализацию метода называемого from_users_followed_by, который мы будем использовать следующим образом:

Micropost.from_users_followed_by(user)

Хотя мы пока не знаем как реализовать его, мы уже можем написать тесты для его функциональности. Ключевым моментом является проверка всех трех требований к потоку: микросообщения читаемых пользователей и самого пользователя должны быт включены в поток, но в него не должны попадать сообщения пользователей, от чтения которых пользователь отписался. Первые два требования уже представлены в наших тестах: Листинг 10.35 проверяет что собственные микросообщения пользователя представлены в потоке, в то время как микросообщений отписанных пользователей быть не должно. Теперь, когда мы знаем как следить за сообщениями пользователей, мы можем добавить третий тип тестов, в этот раз проверяющих что микросообщения читаемых пользователей представлены в потоке, как это показано в Листинге 11.41.

require 'spec_helper'

describe User do
  .
  .
  .
  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
      let(:followed_user) { FactoryGirl.create(:user) }

      before do
        @user.follow!(followed_user)
        3.times { followed_user.microposts.create!(content: "Lorem ipsum") }
      end

      its(:feed) { should include(newer_micropost) }
      its(:feed) { should include(older_micropost) }
      its(:feed) { should_not include(unfollowed_post) }
      its(:feed) do
        followed_user.microposts.each do |micropost|
          should include(micropost)
        end
      end
    end
  end
  .
  .
  .
end

Сам поток сообщений просто перекладывает тяжелую работу на Micropost.from_users_followed_by, как это показано в Листинге 11.42.

class User < ActiveRecord::Base
  .
  .
  .
  def feed
    Micropost.from_users_followed_by(self)
  end
  .
  .
  .
end

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

Пришло время реализовать Micropost.from_users_followed_by, который мы для простоты будем называть "поток". Поскольку конечный результат довольно сложен, мы будем строить итоговую реализацию потока по кусочкам.

В первую очередь нужно подумать о том, какой вид запроса нам нужен. Что мы хотим сделать, это выбрать из таблицы microposts все микросообщения с id соответствующими пользователям, читаемыми данным пользователем (или самому пользователю). Мы можем схематически написать это следующим образом:

SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>

При написании этого кода мы предположили, что SQL поддерживает ключевое слово IN, что позволяет нам протестировать множественное включение. (К счастью это так.)

Вспомним из предварительной реализации потока в Разделе 10.3.3 что Active Record использует where метод для осуществления вида выбора, показанного выше, что иллюстрирует Листинг 10.36. Там наша выборка была очень простой; мы просто взяли все микросообщения с user id соответствующим текущему пользователю:

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

Здесь мы ожидаем, что он будет более сложным, чем то вроде

where("user_id in (?) OR user_id = ?", following_ids, user)

(Здесь мы в состоянии использовать, согласно Rails конвенции, user вместо user.id; Rails автоматически использует id. Мы также опустили впереди идущую часть Micropost. поскольку мы ожидаем что этот метод будет жить в самой модели Micropost.)

Мы видим из этих условий, что нам нужен массив id пользователей, читаемых данным пользователем (или какой-то эквивалент). Один из способов сделать это заключается в использовании Ruby метода map, доступного на любом "перечисляемом" объекте, т.е., любом объекте (таком как Массив или Хэш), который состоит из коллекции элементов.Основное требование заключается в том, что перечисляемые объекты должны реализовывать each метод для перебора коллекции. Мы видели пример этого метода в Разделе 4.3.2; он работает следующим образом:

$ rails console
>> [1, 2, 3, 4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]

Ситуации, подобные той, что показана выше, где такой же метод (например, На самом деле такая нотация на самом деле изначально была расширением которое Rails вносил в ядро ??языка Ruby; она была настолько полезной, что в настоящее время она включена в сам Ruby. Замечательно, правда?

>> [1, 2, 3, 4].map(:to_s)
=> ["1", "2", "3", "4"]

Используя метод join (Раздел 4.3.1), мы можем создать строку состоящую из id объединив их через запятую-пробел:

>> [1, 2, 3, 4].map(:to_s).join(', ')
=> "1, 2, 3, 4"

Мы можем использовать вышеприведенный метод для построения необходимого массива id читаемых пользователей вызвав id на каждом элементе в user.followed_users. Например, для первого пользователя в базе данных этот массив выглядит следующим образом:

>> User.first.followed_users.map(:id)
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

Фактически, так как конструкции такого вида очень полезны, Active Record обеспечивает ее по умолчанию:

>> User.first.followed_user_ids
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

Здесь метод followed_user_ids синтезирован библиотекой Active Record на основе ассоциации has_many :followed_users (Листинг 11.10); в результате, для получения id соответствующих коллекции user.followed_users, нам достаточно добавить _ids к названию ассоциации. Строка id читаемых пользователей тогда будет выглядеть следующим образом:

>> User.first.followed_user_ids.join(', ')
=> "4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51"

Однако при вставке строки в SQL, вам нет надобности делать этого; интерполяция ? заботится об этом за вас (и фактически устраняет некоторые несовместимости связанные с базой данных). Это означает что мы можем использовать

user.followed_user_ids

само по себе.

В этой точке вы можете догадаться что код вроде

Micropost.from_users_followed_by(user)

будет включать в себя метод класса в Micropost классе (конструкция кратко упоминавшаяся в Разделе 4.4.1). Предполагаемая реализация с этими строками представлена в Листинге 11.43.

class Micropost < ActiveRecord::Base
  .
  .
  .
  def self.from_users_followed_by(user)
    followed_user_ids = user.followed_user_ids
    where("user_id IN (?) OR user_id = ?", followed_user_ids, user)
  end
end

Хотя обсуждение ведущее к Листингу 11.43 было выдержано в гипотетических тонах, он действительно работает! Вы можете проверить это запустив набор тестов, которые должны пройти:

$ bundle exec rspec spec/

В большинстве приложений этой начальной реализации было бы вполне достаточно для большинства практических целей. Но это не финальная реализация; посмотрим, сможете ли вы догадаться почему, прежде чем перейти к следующему разделу. (Намек: А что если пользователь следит за сообщениями 5000 других пользователей?)

Подзапросы

Как намекалось в последнем разделе, реализация потока сообщений в Section 11.3.2 не очень хорошо масштабируется при большом количестве микросообщений, что скорее всего произойдет если пользователь начнет читать сообщения, скажем, 5000 других пользователей. В этом разделе мы повторно реализуем ленту сообщений способом, который лучше масштабируется с количеством читаемых пользователей.

Проблема с кодом из Раздела 11.3.2 в том что

followed_user_ids = user.followed_user_ids

вытягивает всех читаемых пользователей в память и создает массив длинной во весь список читаемых пользователей. Поскольку условие в Листинге 11.43 на самом деле лишь проверяет включение во множество, должен быть более эффективный способ для этого, да и SQL оптимизирован именно для таких множественных операций. Решение заключается в отправке поиска id читаемых пользователей в базу данных с помощью подзапроса.

Мы начнем с рефакторинга потока немного модифицированным кодом в Листинге 11.44.

class Micropost < ActiveRecord::Base
  .
  .
  .
  # Returns microposts from the users being followed by the given user.
  def self.from_users_followed_by(user)
    followed_user_ids = user.followed_user_ids
    where("user_id IN (:followed_user_ids) OR user_id = :user_id",
          followed_user_ids: followed_user_ids, user_id: user)
  end
end

В качестве подготовки к следующему шагу мы заменили

where("user_id IN (?) OR user_id = ?", followed_user_ids, user)

на эквивалентное

where("user_id IN (:followed_user_ids) OR user_id = :user_id",
      followed_user_ids: followed_user_ids, user_id: user)

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

Обсуждение выше привело нас к тому что мы добавим второе вхождение user_id в SQL запросе. В частности, мы можем заменить Ruby код

followed_user_ids = user.followed_user_ids

на фрагмент SQL

followed_user_ids = "SELECT followed_id FROM relationships
                     WHERE follower_id = :user_id"

Этот код содержит подзапрос SQL и внутренне вся выборка для пользователя 1 будет выглядеть примерно так:

SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
                  WHERE follower_id = 1)
      OR user_id = 1

Этот подзапрос организует всю логику для отправки в базу данных, что является более эффективным.Для более продвинутых способов создания необходимых подзапросов, см. сообщение в блоге "Hacking a subselect in ActiveRecord".

С этим фундаментом мы готовы к эффективной релизации потока сообщений, как видно в Листинге 11.45. Обратите внимание, что, так как теперь это чистый SQL, followed_user_ids является интерполированным, а не маскированным. (На самом деле рабочими являются оба варианта, но мне кажется более логичным интерполировать в данном контексте.)

class Micropost < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order('created_at DESC') }
  validates :content, presence: true, length: { maximum: 140 }
  validates :user_id, presence: true

  # Returns microposts from the users being followed by the given user.
  def self.from_users_followed_by(user)
    followed_user_ids = "SELECT followed_id FROM relationships
                         WHERE follower_id = :user_id"
    where("user_id IN (#{followed_user_ids}) OR user_id = :user_id",
          user_id: user.id)
  end
end

Этот код представляет собой внушительную комбинацию Rails, Ruby, и SQL, но он делает свою работу и делает ее хорошо. (Конечно же, даже подзапрос не является универсальным решением для масштабирования. Для бОльших сайтов, вам, вероятно, потребуется генерировать поток асинхронно с помощью фонового процесса. Такие тонкости масштабирования выходят за рамки данного руководства.)

Новый поток сообщений

С кодом в Листинге 11.45, наш поток сообщений завершен. Напомним, что код для Home страницы, представлен в Листинге 11.46; этот код создает пагинированный поток соответствущих микросообщений для использования в представлении, как видно в рис 11.45" type="example">Листинге 11.45, наш поток сообщений завершен. Напомним, что код для Home страницы, представлен в Листинге 11.46; этот код создает пагинированный поток соответствущих микросообщений для использования в представлении, как видно в Для того чтобы сделать поток сообщений на рис 11.20 более привлекательным, я добавил несколько дополнительных микросообщений вручную используя Rails консоль. Отметим, что paginate метод фактически достигает цели в методе модели Micropost в Листинге 11.45, организуя вытягивание только 30 микросообщений за раз из базы данных. (Вы можете проверить это изучив SQL выражения в логах сервера разработки.)

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
(рис 11.20) Home страница с работающим потоком сообщений.

Заключение

Добавив ленту сообщений, мы закончили ключевой пример приложения Учебника Ruby on Rails. Это приложение включает в себя примеры всех основных возможностей Rails, включая модели, представления, контроллеры, шаблоны, партиалы, фильтры, валидации, обратные вызовы, has_many/belongs_to и has_many through ассоциации, безопасность, тестирование и развертывание. Несмотря на этот внушительный список, вам предстоит еще очень многое узнать о Rails. В качестве первого шага на этом пути, этот раздел содержит некоторые рекомендуемые расширения основного приложения, а также рекомендации для дальнейшего обучения.

Прежде чем перейти к решению любого из предложенных расширений приложения, хорошо бы объединить ваши изменения:

$ git add .
$ git commit -m "Add user following"
$ git checkout master
$ git merge following-users

Как обычно, если хотите, вы можете также отправить ваше приложение и развернуть его на сервере:

$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:populate

Расширения к примеру приложения

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

Не удивляйтесь, если по началу будет непросто; чистый лист новой фичи может быть немного пугающим. Чтобы помочь вам начать, я могу дать две большие рекомендации. Во-первых, пежде чем добавлять какую-либо функцию к Rails приложению, взгляните на RailsCasts archive чтобы посмотреть, не рассказал ли уже об этом Ryan Bates.Единственная моя оговорка по поводу Railscasts — они обычно опускают тесты. Это, вероятно, необходимо для сохранения красоты и краткости эпизодов, но у вас может сформироваться неправильное представление о важности тестов. После просмотра соответствующего Railscast для получения представления о процессе, я советую писать новую фнкцию используя разработку через тестирование. (В этом контексте я рекомендую взглянуть на the RailsCast on "How I test". Вы увидите, что Ryan Bates сам обычно использует TDD для разработки в реальной жизни и что, фактически, его стиль тестирования совпадает со стилем используемым в данном учебнике.) Если он это сделал, просмотр соответствующего Railscast сэкономит вам массу времени. Во-вторых, всегда делайте обширный поиск в Google по вашей предполагаемой функции, чтобы найти соответствующее сообщения в блогах и пособиях. Разработка веб приложений это непростое дело и это поможет вам учиться на чужом опыте (и ошибках).

Многие из следующих функций являются довольно непростыми задачами, и я дал несколько подсказок по поводу средств, которые могут вам понадобиться для их реализации. Даже с подсказками, они являются намного более трудными чем упражнения, которые приводились в конце каждой главы учебника, так что не расстраивайтесь, если вы не можете решить их без значительных усилий. Из-за нехватки времени я недоступен для личной помощи, но если есть достаточный интерес я мог бы выпустить автономную статью/скринкаст охватывающий эти расширения в будущем; перейдите на основной сайт Rails Tutorial http://railstutorial.org/ и подпишитесь на ленту новостей, чтобы быть в курсе последних обновлений.

Реплики

Твиттер позволяет пользователям делать "@replies", которые являются микросообщениями, чьи первые символы являются логином пользователя предшествующим знаку @. Эти сообщения появляются только в потоке сообщений у пользователя задавшего вопрос или у пользователей читающих данного пользователя. Реализуйте упрощенную версию этого, ограничив появление @replies только в потоках сообщений получателя и отправителя. Это может подразумевать добавление in_reply_to столбца в таблицу microposts и дополнительного including_replies пространства к модели Micropost

Поскольку нашему приложению не хватает уникальных пользовательских логинов, вам также необходимо решить, каким способом представлять пользователей. Один из вариантов это использование комбинации id и имени, например @1-michael-hartl. Другой способ это добавить уникальное имя пользователя в процесс регистрации и затем использовать его в @replies.

Обмен сообщениями

Твиттер поддерживает непосредственный (приватный) обмен сообщениями с помощью добавления префикса с буквой "d" к микросообщению. Реализуйте эту функцию для примера приложения. Решение, вероятно, подразумевает наличие модели Message и проверку новых микросообщений с помощью регулярных выражений.

Уведомления о новых читателях

Реализуйте функцию, отправляющую каждому пользователю email уведомление когда у него появляется новый читатель. Затем сделайте уведомления необязательными, так чтобы пользователи могли отказаться при желании. Помимо всего прочего, добавление этой функции требует знания о том, как отправлять почту с помощью Rails. Начните с RailsCast on Action Mailer in Rails 3.

Напоминание пароля

В настоящее время, если пользователи нашего приложения забудут свои пароли, они не смогут их восстановить. Из-за одностороннего безопасного хэширования паролей в Главе 6, наше приложение не может отправить по email пароли пользователей, но оно может отправить ссылку на форму сброса пароля. Используя RailsCast on Remember Me Reset Password в качестве примера, исправьте это упущение.

Подтверждение регистрации

Помимо регулярного выражения для электронной почты, пример приложения в настоящее время не имеет способа проверки валидности пользовательского email адреса. Добавьте шаг проверки email адреса в подтверждение регистрации пользователя. Новая функция должна создавать пользователей в неактивном состоянии, отправлять по email пользователям активационный URL, а затем активировать статус пользователя при посещении соответствующего URL. Для работы с активный/неактивный переходами вам может помочь прочтение state machines in Rails.

RSS канал

Реализовать для каждого пользователя RSS канал их микросообщений. Затем реализовать RSS канал для их лент сообщений, опционально ограничив доступ к этому каналу используя аутентификационную схему. RailsCast on generating RSS feeds поможет вам начать.

REST API

Многие веб сайты раскрывают Application Programmer Interface (API) так что сторонние приложения могут get, post, put и delete ресурсы приложения. Реализуйте такой REST API для примера приложения. Решение подразумевает добавление respond_to блоков (Раздел 11.2.5) ко многим действиям Application контроллера; они должны отвечать на запросы для XML. Позаботьтесь о безопасности; API должен быть доступен только авторизированным пользователям.

Поиск

В настоящее время у пользователей нет другого способа найти друг-друга, кроме как просмотром списка пользователей или просматривая потоки сообщений других пользователей. Реализуйте функцию поиска чтобы исправить ситуацию. Затем добавьте другую поисковую функцию для микросообщений. RailsCast on simple search forms поможет вам начать. Если вы используете шаред хостинг или выделенный сервер, я советую использовать Thinking Sphinx (см. RailsCast on Thinking Sphinx). Если вы развернуты на Heroku, вы должны следовать инструкциям Heroku full text search.

Руководство по дальнейшим ресурсам

Существует огромное количество Rails ресурсов в магазинах и в сети — предложений настолько много что это может ошеломить. Хорошие новости заключаются в том, что, если вы дошли до этого места учебника, вы готовы практически ко всему за его пределами. Вот несколько советов:

  • The Ruby on Rails Tutorial screencasts: я подготовил полноценный скринкаст курс основанный на этой книге. В дополнение к раскрытию всех материалов этой книги, скринкасты дополнены советами, трюками и демонстрациями типа смотри-как-это-делается, которые сложно зафиксировать в печатном варианте. Их можно приобрести через сайт Ruby on Rails Tutorial.
  • RailsCasts: трудно переоценить важность ресурса Railscasts, я советую начать с посещения архива эпизодов Railscasts и клика по любой зацепившей вас теме.
  • Ruby и Rails книги: я рекомендую Beginning Ruby Петера Купера, The Well-Grounded Rubyist David A. Black, Eloquent Ruby Russ Olsen и The Ruby Way Хэла Фултона для дальнейшего изучения Ruby, и The Rails 3 Way Оби Фернандеса, и Rails 3 in Action (подождите второго издания) Ryan Bigg и Yehuda Katz для изучения Rails.
  • PeepCode и Code School: скринкасты от PeepCode и интерактивные курсы в Code School неизменно высокого качества и я горячо рекомендую их.
  • Упражнения

    Добавьте тесты для уничтожения взаимоотношений связанных с данным пользователем (т.e., к реализации dependent :destroy в Листинге 11.4 и Листинге 11.16). Подсказка: следуйте примеру в Листинге 11.12.

    Метод respond_to виденый в Листинге 11.38 на самом деле может быть поднят из действий в сам контроллер Relationships, и respond_to блоки могут быть заменены на Rails метод respond_with. Подтвердите, что результирующий код, показаный в Листинге 11.47, является корректным, проверив что набор тестов все еще проходит. (Подробнее об этом методе см в Google поиске "rails respond_with".)

    Сделайте рефакторинг Листинга 11.31 добавив партиалы для кода общего для страниц following/followers, страницы Home, и страницы показывающей пользователя.

    Следуя модели в Листинге 11.19, напишите тесты для статистики на странице профиля.

    class RelationshipsController < ApplicationController
      before_action :signed_in_user
    
      respond_to :html, :js
    
      def create
        @user = User.find(params[:relationship][:followed_id])
        current_user.follow!(@user)
        respond_with @user
      end
    
      def destroy
        @user = Relationship.find(params[:id]).followed
        current_user.unfollow!(@user)
        respond_with @user
      end
    end
    Страницы:

    В этой главе мы завершим ядро примера приложения, добавив социальный слой, что позволит пользователям читать (и не читать) сообщения других пользователей, в результате чего на главной странице каждого пользователя будет отображаться список микросообщений тех пользователей, сообщения которых он читает. Мы также сделаем представления для отображения читающих и читаемых пользователей. Мы узнаем, как смоделировать взаимоотношения между пользователями в Разделе 11.1, а затем сделаем веб-интерфейс в Разделе 11.2 (включая введение в Ajax). Наконец, мы закончим, разработав полнофункциональный поток сообщений в Разделе 11.3.

    Эта последняя глава содержит несколько из наиболее сложных материалов учебника, в том числе, сложные модели данных и несколько Ruby / SQL хитростей для создания потока сообщений. С помощью этих примеров вы увидите как Rails может обрабатывать даже весьма сложные модели данных, что должно вам пригодиться, так как вы двигаетесь к разработке собственных приложений с их собственными требованиями. Чтобы помочь с переходом от учебника к самостоятельной разработке, Раздел 11.4 содержит рекомендуемые расширения к ядру примера приложения, а также ссылки на более продвинутые ресурсы.

    Как обычно, Git пользователи должны создать новую тему ветки:

    $ git checkout -b following-users

    Так как материал этой главы особенно сложен, прежде чем писать код, мы улучим момент и сделаем небольшой обзор интерфейса. Как и в предыдущих главах, на этом раннем этапе мы будем представлять страницы используя наброски.Фотографии для набросков взяты с http://www.flickr.com/photos/john_lustig/2518452221/ и http://www.flickr.com/photos/30775272@N05/2884963755/ Полная последовательность страниц работает следующим образом: пользователь, (John Calvin) начинает на странице своего профиля (рис 11.1) и переходит на страницу со списком пользователей (рис 11.2) для того, чтобы выбрать пользователя, сообщения которого он будет читать. Calvin переходит на страницу профиля выбранного пользователя, Thomas-а Hobbes-а (рис 11.3), кликает по кнопке "Follow", чтобы читать сообщения этого пользователя. Это изменяет кнопку "Follow" на "Unfollow", и увеличивает количество "followers" товарища Hobbes-а на единицу (рис 11.4). Вернувшись на свою главную страницу, Calvin теперь видит увеличившееся количество "following" и обнаруживает микросообщения Hobbes-а в своем потоке сообщений (рис 11.5). Остальная часть этой главы посвящена реализации этой последовательности.

    (рис 11.1) Профиль текущего пользователя. (рис 11.2) Поиск пользователя для чтения его сообщений. (рис 11.3) Профиль другого пользователя с “Follow” кнопкой. (рис 11.4) Профиля с “Unfollow” кнопкой и увеличившимся количеством читателей. (рис 11.5) Главная страница с лентой сообщений и увеличившимся количеством читаемых пользователей.

    Модель Relationship

    Наш первый шаг в реализации слежения за сообщениями пользователей, заключается в построении модели данных, которая не так проста, как кажется. Naively, кажется, что has_many отношение должно сработать: пользователь has_many (имеет_много) читаемых и has_many (имеет_много) читателей. Как мы увидим, в этом подходе есть проблема, и мы узнаем как ее исправить используя has_many through. Вполне вероятно, что многие идеи этого раздела окажутся непонятыми с первого раза, и может потребоваться некоторое время для осознания довольно сложной модели данных. Если вы обнаружите что запутались, попробуйте пройти главу до конца, а затем прочитать этот раздел еще раз, чтобы прояснить для себя некоторые вещи.

    Проблема с моделью данных (и ее решение)

    В качестве первого шага на пути построения модели данных для слежения за сообщениями пользователей, давайте рассмотрим следующий типичный случай. Возьмем, в качестве примера, пользователя, который следит за сообщениями второго пользователя: мы могли бы сказать, что, например, Кальвин читает сообщения Гоббса, и Гоббс читается Кальвином, таким образом, Кальвин является читателем, а Гоббс является читаемым. При использовании дефолтной Rails’ плюрализации, множество таких читаемых пользователей называлось бы followers, и Первое издание этой книги использовало терминологию user.following, в которой даже я иногда путался. Благодарю читателя Cosmo Lee за то что он убедил меня изменить терминологию и за сформулированные советы о том как сделать это более понятным. (Однако не последовал его совету в точности, так что если вы все еще путаетесь - это не его вина.)

    Это предполагает моделирование читаемых пользователей как на рис 11.6, с Для простоты, рис 11.6 подавляет id столбец таблицы following. Кроме того, так как каждая строка является пользователем, мы должны были бы включить другие атрибуты пользователя, включая имя, пароль и т.д.

    (рис 11.6) Наивная реализация слежения за сообщениями пользователя.

    Проблема модели данных из рис 11.6 в том, что она ужасно избыточна: каждая строка содержит не только id каждого читаемого пользователя, но и всю остальную информацию, уже содержащуюся в таблице users. Еще хуже то, что для моделирования читателей пользователя нам потребуется отдельная followers таблица. Наконец, эта модель данных кошмарно неудобна в эксплуатации, так как каждый раз при изменении пользователем (скажем) своего имени, нам пришлось бы обновлять запись пользователя не только в users таблице, но также каждую строку, содержащую этого пользователя в обоих followed_users и followers таблицах.

    Проблема здесь в том, что нам не хватает лежащей в основе абстракции. Один из способов найти правильную абстракцию, это рассмотреть, как мы могли бы реализовать чтение сообщений пользователя в веб-приложении. Вспомним из Раздела 7.1.2, что REST архитектура включает в себя ресурсы которые создаются и уничтожаются. Это приводит нас к двум вопросам: Что создается, когда пользователь начинает читать сообщения другого пользователя? Что уничтожается, когда пользователь прекращает следить за сообщениями другого пользователя?

    Поразмыслив, мы видим, что в этих случаях приложение должно создать либо разрушить взаимоотношение между двумя пользователями. Затем пользователь уже содержит большую часть реализации: поскольку каждый читаемый пользователь уникально идентифицирован посредством followed_id, мы можем преобразовать followed_users в таблицу relationships, опустив информацию о пользователе, и использовав followed_id для получения читаемых пользователей из users таблицы. Кроме того, приняв во внимание обратные взаимоотношения, мы могли бы использовать follower_id столбец для извлечения массива читателей пользователя.

    Для того, чтобы создать массив пользователей followed_users, мы могли бы вытянуть массив атрибутов followed_id, а затем найти пользователя для каждого из них. Однако, как и следовало ожидать, в Rails есть более удобный способ для этой процедуры; соответствующая техника известна как has_many through. Как мы увидим в Разделе 11.1.4, Rails позволяет нам сказать, что пользователь следит за сообщениями многих пользователей через таблицу взаимоотношений, используя краткий код

    has_many :followed_users, through: :relationships, source: :followed

    Этот код автоматически заполняет .

    (рис 11.7) Модель слежения пользователя за сообщениями через взаимоотношения.

    Чтобы начать работу над реализацией, мы сначала генерируем модель Relationship следующим образом:

    $ rails generate model Relationship follower_id:integer followed_id:integer

    Возможно при этом была сгенерирована фабрика Relationship которую вам следует удалить:

    $ rm -f spec/factories/relationship.rb

    Так как мы будем искать взаимоотношения по follower_id и по followed_id, мы должны добавить индекс на каждой колонке для повышения эффективности поиска, как показано в Листинге 11.1.

    class CreateRelationships < ActiveRecord::Migration
      def change
        create_table :relationships do |t|
          t.integer :follower_id
          t.integer :followed_id
    
          t.timestamps
        end
        add_index :relationships, :follower_id
        add_index :relationships, :followed_id
        add_index :relationships, [:follower_id, :followed_id], unique: true
      end
    end

    Листинге 11.1 включает также составной индекс, который обеспечивает уникальность пар (follower_id, followed_id), так что пользователь не может следить за сообщениями другого пользователя более одного раза:

    add_index :relationships, [:follower_id, :followed_id], unique: true

    (Сравните с индексом уникальности email из Листинга 6.9.) Как мы увидим в Разделе 11.1.4, наш пользовательский интерфейс не позволит этому случиться, но добавление индекса уникальности позволит избежать ошибки в случае, если пользователь попытается дублировать взаимотношения любым другим способом (используя, например, инструмент командной строки, такой как curl). Мы могли бы также добавить валидацию уникальности к модели Relationship, но, так как дублирование взаимоотношений является ошибкой всегда, для наших целей вполне достаточно индекса уникальности.

    Для создания таблицы relationships, мы мигрируем базу данных и подготавливаем тестовую бд, как обычно:

    $ bundle exec rake db:migrate
    $ bundle exec rake test:prepare

    Результирующая модель данных Relationship показана на рис 11.8.

    (рис 11.8)

    Ассоциации пользователь/взаимоотношение

    Прежде чем приступить к реализации читателей и читаемых, нам вначале необходимо установить ассоциацию между пользователями и взаимоотношениями. Пользователь has_many (имеет_много) взаимоотношений, и, так как взаимоотношения включают двух пользователей — взаимоотношение belongs_to (принадлежит_к) читающим и читаемым пользователям.

    Как и с микросообщениями в Разделе 10.1.3, мы будем создавать новые взаимоотношения используя ассоциацию, с помощью такого кода

    user.relationships.build(followed_id: ...)

    Мы начнем с базовых тестов валидации показанных в Листинге 11.2.

    require 'spec_helper'
    
    describe Relationship do
    
      let(:follower) { FactoryGirl.create(:user) }
      let(:followed) { FactoryGirl.create(:user) }
      let(:relationship) { follower.relationships.build(followed_id: followed.id) }
    
      subject { relationship }
    
      it { should be_valid }
    end

    Обратите внимание, что, в отличие от тестов для моделей User и Micropost, которые использовали Более подробно об этом см. when to use let at Stack Overflow. но я считаю let более чистым решением, нежели переменные экземпляра. Ранее мы применяли переменные экземпляра из-за того что нам важно было как можно раньше ввести переменные экземпляра, а также потому что let является немного более продвинутой техникой.

    Мы также должны протестировать атрибут relationships модели User как это показано в Листинге 11.3.

    require 'spec_helper'
    
    describe User do
      .
      .
      .
      it { should respond_to(:feed) }
      it { should respond_to(:relationships) }
      .
      .
      .
    end

    В этой точке вы, возможно, ожидаете код приложения как в Разделе 10.1.3 - и он действительно похож, но есть одно важное отличие: в случае с моделью Micropost мы могли сказать

    class Micropost < ActiveRecord::Base
      belongs_to :user
      .
      .
      .
    end

    и

    class User < ActiveRecord::Base
      has_many :microposts
      .
      .
      .
    end

    поскольку у таблицы Если вы заметили что followed_id также идентифицирует пользователя, и обеспокоены ассиметричным обращением с читателями и читаемыми, вы готовы к любым неожиданностям. Мы займемся этим вопросом в Разделе 11.1.5.

    class User < ActiveRecord::Base
      has_many :microposts, dependent: :destroy
      has_many :relationships, foreign_key: "follower_id", dependent: :destroy
      .
      .
      .
    end

    (Поскольку уничтожение пользователя должно также уничтожить его взаимоотношения мы пошли еще дальше и добавили dependent: :destroy к ассоциации; написание теста на это останется в качестве упражнения (Section 11.5).)

    Как и у модели Micropost, у Relationship модели есть belongs_to взаимоотношения с пользователями; в данном случае, объект взаимоотношение принадлежит к обоим follower и followed пользователям, что мы и тестируем в Листинге 11.5.

    describe Relationship do
      .
      .
      .
      describe "follower methods" do
        it { should respond_to(:follower) }
        it { should respond_to(:followed) }
        its(:follower) { should eq follower }
        its(:followed) { should eq followed }
      end
    end

    Чтобы написать код приложения, мы определяем belongs_to взаимоотношения как обычно. Rails выводит названия внешних ключей из соответствующих символов (т.e., follower_id из :follower, и followed_id из :followed), но, так как нет ни Followed ни Follower моделей, мы должны снабдить их именем класса User. Результат показан в Листинге 11.6. Обратите внимание, что, в отличие от дефолтно сгенерированной модели Relationship, в данном случае доступным является только followed_id.

    class Relationship < ActiveRecord::Base
      belongs_to :follower, class_name: "User"
      belongs_to :followed, class_name: "User"
    end

    Ассоциация followed на самом деле не потребуется до Раздела 11.1.5, но параллельность структуры читатели/читаемые лучше видна при одновременной реализации.

    В этой точке тесты из Листинга 11.2 и Листинга 11.3 должны пройти.

    $ bundle exec rspec spec/

    Валидации

    Прежде чем двигаться дальше, мы добавим пару валидаций модели Relationship для комплектности. Тесты (Листинге 11.7) и код приложения (Листинг 11.8) просты.

    describe Relationship do
      .
      .
      .
      describe "when followed id is not present" do
        before { relationship.followed_id = nil }
        it { should_not be_valid }
      end
    
      describe "when follower id is not present" do
        before { relationship.follower_id = nil }
        it { should_not be_valid }
      end
    end
    class Relationship < ActiveRecord::Base
      belongs_to :follower, class_name: "User"
      belongs_to :followed, class_name: "User"
      validates :follower_id, presence: true
      validates :followed_id, presence: true
    end

    Читаемые пользователи

    Теперь мы переходим к сердцу ассоциаций Relationship: followed_users и followers. Мы начнем с followed_users, как показано в Листинге 11.9.

    require 'spec_helper'
    
    describe User do
      .
      .
      .
      it { should respond_to(:relationships) }
      it { should respond_to(:followed_users) }
      .
      .
      .
    end

    Реализация впервые использует . По умолчанию, в ассоциации has_many through Rails ищет внешний ключ, соответствующий ассоциации в единственном числе; другими словами, код

    has_many :followeds, through: :relationships

    будет составлять массив, используя followed_id в таблице relationships. Но, как отмечалось в Разделе 11.1.1, user.followeds это довольно неуклюже; гораздо более естественным будет использование "followed users" в качестве множественного числа для "followed", и написание user.followed_users для массива читаемых пользователей. Естественно, Rails позволяет переопределить умолчание, в данном случае с помощью :source параметра (Листинг 11.10), который явно говорит Rails, что источником массива followed_users является множество followed ids.

    class User < ActiveRecord::Base
      has_many :microposts, dependent: :destroy
      has_many :relationships, foreign_key: "follower_id", dependent: :destroy
      has_many :followed_users, through: :relationships, source: :followed
      .
      .
      .
    end

    Для того, чтобы создавать взаимоотношение с читаемым (пользователем), мы введем служебный метод Если у вас есть большой опыт моделирования конкретной предметной области, вы зачастую можете предугадать такие вспомогательные методы, и даже если нет, вы часто обнаруживаете себя за их написанием с целью почистить тесты. Однако в данном случае нормально если вы не угадали их. Разработка програмного обеспечения это обычно итеративный процесс — вы пишете код до тех пор пока он не начинает становиться уродливым, а затем вы рефакторите его — но, для краткости, изложение в учебнике немного сглажено. Тесты в Листинге 11.11 показывают как мы планируем использовать эти методы на практике.

    require 'spec_helper'
    
    describe User do
      .
      .
      .
      it { should respond_to(:followed_users) }
      it { should respond_to(:following?) }
      it { should respond_to(:follow!) }
      .
      .
      .
      describe "following" do
        let(:other_user) { FactoryGirl.create(:user) }
        before do
          @user.save
          @user.follow!(other_user)
        end
    
        it { should be_following(other_user) }
        its(:followed_users) { should include(other_user) }
      end
    end

    В коде приложения, метод following? принимает пользователя, названного other_user и проверяет, существует ли он в базе данных; метод follow! вызывает create! через relationships ассоциацию для создания взаимоотношения с читаемым. Результаты представлены в Листинге 11.12.

    class User < ActiveRecord::Base
      .
      .
      .
      def feed
        .
        .
        .
      end
    
      def following?(other_user)
        relationships.find_by(followed_id: other_user.id)
      end
    
      def follow!(other_user)
        relationships.create!(followed_id: other_user.id)
      end
      .
      .
      .
    end

    Отметим, что в Листинге 11.12 мы опустили самого пользователя, написав просто

    relationships.create!(...)

    вместо эквивалентного кода

    self.relationships.create!(...)

    Явное включение или невключение self в данном случае дело вкуса.

    Конечно, пользователи должны иметь возможность прекратить слежение за сообщениями других пользователей, что приводит нас к немного предсказуемому методу unfollow!, как показано в Листинге 11.13.

    require 'spec_helper'
    
    describe User do
      .
      .
      .
      it { should respond_to(:follow!) }
      it { should respond_to(:unfollow!) }
      .
      .
      .
      describe "following" do
        .
        .
        .
        describe "and unfollowing" do
          before { @user.unfollow!(other_user) }
    
          it { should_not be_following(other_user) }
          its(:followed_users) { should_not include(other_user) }
        end
      end
    end

    Код для unfollow! прост: нужно просто найти взаимоотношение по followed id и уничтожить его (Листинг 11.14).

    class User < ActiveRecord::Base
      .
      .
      .
      def following?(other_user)
        relationships.find_by(followed_id: other_user.id)
      end
    
      def follow!(other_user)
        relationships.create!(followed_id: other_user.id)
      end
    
      def unfollow!(other_user)
        relationships.find_by(followed_id: other_user.id).destroy!
      end
      .
      .
      .
    end

    Читатели пользователя

    Последней частью пазла взаимоотношений является метод что все сведения, необходимые для извлечения массива читателей уже присутствуют в таблице ), то мы бы с легкостью реализовали user.followers.

    (рис 11.9) Модель данных для читателей пользователя, использующая реверсированную модель Relationship.

    Начнем с тестов, веря, что магия Rails выручит нас (когда дело дойдет до реализации) (Листинг 11.15).

    require 'spec_helper'
    
    describe User do
      .
      .
      .
      it { should respond_to(:relationships) }
      it { should respond_to(:followed_users) }
      it { should respond_to(:reverse_relationships) }
      it { should respond_to(:followers) }
      .
      .
      .
    
      describe "following" do
        .
        .
        .
        it { should be_following(other_user) }
        its(:followed_users) { should include(other_user) }
    
        describe "followed user" do
          subject { other_user }
          its(:followers) { should include(@user) }
        end
        .
        .
        .
      end
    end

    Обратите внимание на то, как мы изменили субъект с помощью метода subject, замена @user на other_user, позволяет нам протестировать взаимоотношение с читателями естесственным образом:

    subject { other_user }
    its(:followers) { should include(@user) }

    Как вы наверное подозреваете, мы не будем создавать полную таблицу в базе данных только для того чтобы просто произвести реверс взаимоотношений. Вместо этого мы воспользуемся базовой симметрией между читаемыми и читателями для симуляции таблицы reverse_relationships, передав followed_id в качестве внешнего ключа. Иными словами, там где ассоциация relationships использует внешний ключ follower_id,

    has_many :relationships, foreign_key: "follower_id"

    ассоциация reverse_relationships использует followed_id:

    has_many :reverse_relationships, foreign_key: "followed_id"

    Ассоциация followers затем строится через реверсированные взаимоотношения, как показано в Листинге 11.16.

    class User < ActiveRecord::Base
      .
      .
      .
      has_many :reverse_relationships, foreign_key: "followed_id",
                                       class_name:  "Relationship",
                                       dependent:   :destroy
      has_many :followers, through: :reverse_relationships, source: :follower
      .
      .
      .
    end

    (Как и с Листингом 11.4, тест для dependent :destroy остается в качестве упражнения (Раздел 11.5).) Обратите внимание, что мы должны включить имя класса для этой ассоциации, т.e.,

    has_many :reverse_relationships, foreign_key: "followed_id",
                                     class_name: "Relationship"

    потому что иначе Rails будет искать несуществующий класс ReverseRelationship.

    Стоит также отметить, что мы могли бы в этом случае пропустить :source, используя просто

    has_many :followers, through: :reverse_relationships

    поскольку Rails будет автоматически искать внешний ключ follower_id в данном случае. Я сохранил ключ :source для того чтобы подчеркнуть параллельность со структурой ассоциации has_many :followed_users, но вы можете пропустить его.

    С кодом в Листинге 11.16, ассоциации читаемые/читатели завершены, и все тесты должны пройти:

    $ bundle exec rspec spec/

    Этот раздел предъявил довольно высокие требования к вашим навыкам моделирования данных, и это нормально, если для его усвоения потребуется некоторое время. Фактически, одним из самых лучших способов понять ассоциации является их использование в веб интерфейсе, как мы увидим в следующем разделе.

    Веб-интерфейс для читаемых пользователей

    Во введении к этой главе, мы сделали предварительный обзор страниц, необходимых для слежения за сообщениями пользователей. В этом разделе мы реализуем базовый интерфейс и читать/не читать функциональность, показанные в тех макетах. Мы также сделаем отдельные страницы для демонстрации массивов читателей и читаемых. В Разделе 11.3, мы завершим наш пример приложения, добавив поток сообщений пользователя.

    Образцы данных

    Как и в предыдущих главах, нам удобно будет использовать Rake задачу для заполнения базы данных образцами взаимоотношений. Это позволит нам заняться в первую очередь внешним видом страниц, временно отложив серверную часть функциональности.

    Когда мы последний раз видели заполнитель образцов данных в Листинге 11.20, он был немного суматошным. Поэтому мы начнем с определения отдельных методов для создания пользователей и микросообщений, а затем добавим образцы данных взаимоотношений используя новый метод make_relationships. Результаты показаны в Листинге 11.17.

    namespace :db do
      desc "Fill database with sample data"
      task populate: :environment do
        make_users
        make_microposts
        make_relationships
      end
    end
    
    def make_users
      admin = User.create!(name:     "Example User",
                           email:    "example@railstutorial.org",
                           password: "foobar",
                           password_confirmation: "foobar",
                           admin: true)
      99.times do |n|
        name  = Faker::Name.name
        email = "example-#{n+1}@railstutorial.org"
        password  = "password"
        User.create!(name:     name,
                     email:    email,
                     password: password,
                     password_confirmation: password)
      end
    end
    
    def make_microposts
      users = User.all(limit: 6)
      50.times do
        content = Faker::Lorem.sentence(5)
        users.each { |user| user.microposts.create!(content: content) }
      end
    end
    
    def make_relationships
      users = User.all
      user  = users.first
      followed_users = users[2..50]
      followers      = users[3..40]
      followed_users.each { |followed| user.follow!(followed) }
      followers.each      { |follower| follower.follow!(user) }
    end

    Здесь образцы взаимоотношений создаются с помощью кода

    def make_relationships
      users = User.all
      user  = users.first
      followed_users = users[2..50]
      followers      = users[3..40]
      followed_users.each { |followed| user.follow!(followed) }
      followers.each      { |follower| follower.follow!(user) }
    end

    Мы несколько произвольно организовали слежение первого пользователя за сообщениями пользователей с 3 по 51, а затем принудили пользователей с 4 по 41 читать сообщения первого пользователя. Полученных в результате взаимоотношений будет вполне достаточно для разработки интерфейса приложения.

    Чтобы выполнить код Листинга 11.17, заполним базу данных как обычно:

    $ bundle exec rake db:reset
    $ bundle exec rake db:populate
    $ bundle exec rake test:prepare

    Статистика и форма для слежения за сообщениями пользователя

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

    Как было отмечено в Разделе 11.1.1, слово "following" является двусмысленным при использовании в качестве атрибута (где user.following могло бы означать или читаемых пользователей или читателей пользователя), но оно вполне подходит в качестве метки, как в "50 following". Кроме того, это метка которой пользуется сам Twitter, использование принятое в набросках начиная с рис 11.1 и показанное крупным планом на рис 11.10.

    (рис 11.10) Набросок партиала статистики.

    Статистика на рис 11.10 содержит и количество читаемых пользователей, и количество его читателей, каждое из чисел должно быть ссылкой на соответствующую специальную страницу для их отображения. В Главе 5, мы временно заглушали подобные ссылки знаком ’#’, но это было до того, как мы набрались опыта с маршрутами. Сейчас, несмотря на то, что мы отложили сами страницы до Раздела 11.2.3, мы сделаем маршруты сейчас, как показано в Листинге 11.18. Этот код использует метод :member внутри блока resources, с которым мы ранее не были знакомы, но посмотрим, сможете ли вы угадать, что он делает. (Примечание: код в Листинге 11.18 должен заменить resources :users.)

    SampleApp::Application.routes.draw do
      resources :users do
        member do
          get :following, :followers
        end
      end
      .
      .
      .
    end

    Вы возможно догадываетесь, что URL для читаемых и читающих пользователей будут выглядеть как /users/1/following и /users/1/followers, и это именно то, что делает код в Листинге 11.18. Поскольку обе страницы будут отображать данные, мы используем get для того чтобы организовать ответ URL на запросы GET (что требуется конвенцией REST для подобных страниц), и метод member означает, что маршруты отвечают на URL, содержащие id пользователя. (Другой возможный метод, collection, работает без id, так что

    resources :users do
      collection do
        get :tigers
      end
    end

    будет отвечать на URL /users/tigers (presumably to display all the tigers in our application). — предположительно для отображения всех тигров нашего приложения. Узнать больше об этой опции можно из Ruby on Rails по-русски "Роутинг в Rails". Таблица маршрутов, сгенерированных Листинге 11.18 представлена в Таблице 11.1; обратите внимание на именнованные маршруты для страниц с читателями и читаемыми, которые мы вскоре будем использовать. Неудачное гибридное применение в маршруте "following" обусловлено нашим решением использовать недвусмысленную терминологию "followed users" наряду с применением "following" взятым у Twitter. Поскольку предыдущий привел бы нас к маршрутам вида followed_users_user_path, что звучит довольно странно, мы выбрали последний в контексте Таблицы 11.1, что привело к following_user_path.

    RESTful маршруты предоставленные кастомными правилами в ресурсе из Листинга 11.18.
    HTTP request URL Действие Именованный маршрут
    GET /users/1/following following following_user_path(1)
    GET /users/1/followers followers followers_user_path(1)

    Определив маршруты мы готовы сделать тесты партиала статистики. (Мы могли бы начать с тестов, но именованные маршруты было бы трудно объяснить без обновленного файла маршрутов.) Партиал появится на странице профиля и на странице Home;

    require 'spec_helper'
    
    describe "Static pages" do
      .
      .
      .
      describe "Home page" do
        .
        .
        .
        describe "for signed-in users" do
          let(:user) { FactoryGirl.create(:user) }
          before do
            FactoryGirl.create(:micropost, user: user, content: "Lorem")
            FactoryGirl.create(:micropost, user: user, content: "Ipsum")
            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
    
          describe "follower/following counts" do
            let(:other_user) { FactoryGirl.create(:user) }
            before do
              other_user.follow!(user)
              visit root_path
            end
    
            it { should have_link("0 following", href: following_user_path(user)) }
            it { should have_link("1 followers", href: followers_user_path(user)) }
          end
        end
      end
      .
      .
      .
    end

    Ядром этих тестов является предположение, что количество читателей и читаемых представлено на странице совместно с правильными URL:

    it { should have_link("0 following", href: following_user_path(user)) }
    it { should have_link("1 followers", href: followers_user_path(user)) }

    Здесь мы использовали именованные маршруты, показанные в Таблице 11.1 для проверки того, что ссылки имеют правильные адреса. Также обратите внимание на то, что в данном случае слово "followers" работает как метка, так что мы сохраним его во множественном числе, даже если будет только один читатель.

    Код приложения для партиала статистики это просто несколько ссылок внутри div, как показано в Листинге 11.20.

    <% @user ||= current_user %>
    <div class="stats">
      <a href="<%= following_user_path(@user) %>">
        <strong id="following" class="stat">
          <%= @user.followed_users.count %>
        </strong>
        following
      </a>
      <a href="<%= followers_user_path(@user) %>">
        <strong id="followers" class="stat">
          <%= @user.followers.count %>
        </strong>
        followers
      </a>
    </div>

    Поскольку мы включим статистику и на профиле пользователя и на Home странице, первая строка Листинга 11.20 выбирает правильное с помощью

    <% @user ||= current_user %>

    Как обсуждалось в Блоке 8.2, если @user не является nil, то ничего не происходит (как и на странице профиля), но когда он существует (как на странице Home), он назначает @user равным текущему пользователю.

    Обратите внимание также на то, что количество читаемых/читателей подсчитывается через ассоциацию с помощью

    @user.followed_users.count

    и

    @user.followers.count

    Сравните это с подсчетом количества микросообщений из Листинга 11.17, где мы писали

    @user.microposts.count

    для подсчета микросообщений.

    Одна последняя деталь которую стоит отметить - наличие CSS id у некоторых элементов, как в

    <strong id="following" class="stat">
    ...
    </strong>

    Это сделано в угоду Ajax реализации из Раздела 11.2.5, которая получает доступ к элементам страницы используя их уникальные id.

    С готовым партиалом включить статистику в Home страницу проще простого, как показано в Листинге 11.21. (Это также приводит к прохождению тестов из Листинга 11.19.)

    <% if signed_in? %>
          .
          .
          .
          <section>
            <%= render 'shared/user_info' %>
          </section>
          <section>
            <%= render 'shared/stats' %>
          </section>
          <section>
            <%= render 'shared/micropost_form' %>
          </section>
          .
          .
          .
    <% else %>
      .
      .
      .
    <% end %>

    Для того чтобы придать статистике стиль, мы добавим немного SCSS, как это показано в Листинге 11.22 (который содержит весь код таблиц стилей необходимый в этой главе). Результат представлен на рис 11.22" type="example">Листинге 11.22 (который содержит весь код таблиц стилей необходимый в этой главе). Результат представлен на . . . /* sidebar */ . . . .stats { overflow: auto; a { float: left; padding: 0 10px; border-left: 1px solid $grayLighter; color: gray; :first-child { padding-left: 0; border: 0; } :hover { text-decoration: none; color: $blue; } } strong { display: block; } } .user_avatars { overflow: auto; margin-top: 10px; .gravatar { margin: 1px 1px; } } . . . (рис 11.11) Home страница (/) со статистикой.

    Мы подключим статистику к странице профиля через мгновение, но вначале давайте сделааем партиал для follow/unfollow кнопки, как показано в Листинге 11.22.

    <% unless current_user?(@user) %>
      <div id="follow_form">
      <% if current_user.following?(@user) %>
        <%= render 'unfollow' %>
      <% else %>
        <%= render 'follow' %>
      <% end %>
      </div>
    <% end %>

    Этот партиал ничего не делает кроме перекладывания реальной работы на follow и unfollow партиалы, которым нужен новый файл маршрутов чьи правила для ресурса Relationships, следуют примеру ресурса Microposts (Листинг 11.22), как показано в Листинге 11.24.

    SampleApp::Application.routes.draw do
      .
      .
      .
      resources :sessions,      only: [:new, :create, :destroy]
      resources :microposts,    only: [:create, :destroy]
      resources :relationships, only: [:create, :destroy]
      .
      .
      .
    end

    Сами партиалы follow/unfollow показаны в Листинге 11.25 и Листинге 11.26.

    <%= form_for(current_user.relationships.build(followed_id: @user.id)) do |f| %>
      <div><%= f.hidden_field :followed_id %></div>
      <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
    <% end %>
    <%= form_for(current_user.relationships.find_by(followed_id: @user),
                 html: { method: :delete }) do |f| %>
      <%= f.submit "Unfollow", class: "btn btn-large" %>
    <% end %>

    Обе эти формы используют form_for для манипуляций с объектом модели Relationship; основное отличие между ними заключается в том, что Листинг 11.25 строит новое взаимоотношение, тогда как Листинг 11.26 ищет существующее взаимоотношение. Естественно, первый отправляет POST запрос к контроллеру Relationships для create взаимоотношения, в то время как последний отправляет DELETE запрос для destroy взаимоотношения. (Мы напишем эти действия в Разделе 11.2.4.) Наконец, отметьте что форма follow/unfollow не содержит никакого контента кроме кнопки, но нам все еще необходимо отправить followed_id, чего мы можем добиться с помощью метода hidden_field из Листинга 11.25; который производит HTML вида

    <input id="relationship_followed_id"
    name="relationship[followed_id]"
    type="hidden" value="3" />

    Тег "hidden" input поместит соответствующую информацию на странице не отображая ее в браузере.

    Теперь мы можем включить форму чтения и статистику на страницу профиля пользователя простым рендерингом партиалов, как показано в Листинге 11.27. Профили с follow и unfollow кнопками, соответственно, представлены на рис 11.27" type="example">Листинге 11.27. Профили с follow и unfollow кнопками, соответственно, представлены на рис 11.13.

    <% provide(:title, @user.name) %>
    <div class="row">
      <aside class="span4">
        <section>
          <h1>
            <%= gravatar_for @user %>
            <%= @user.name %>
          </h1>
        </section>
        <section>
          <%= render 'shared/stats' %>
        </section>
      </aside>
      <div class="span8">
        <%= render 'follow_form' if signed_in? %>
        .
        .
        .
      </div>
    </div>
    (рис 11.12) Профиль пользователя с follow кнопкой (/users/2). (рис 11.13) Профиль пользователя с unfollow кнопкой (/users/6).

    Мы вскоре сделаем эти кнопки рабочими, фактически, мы сделаем это двумя способами, стандартным способом (Раздел 11.2.4) и с помощью Ajax (Раздел 11.2.5), но вначале мы закончим HTML интерфейс, создав страницы для списков читающих и читаемых.

    Страницы с читаемыми и читателями

    Страницы для отображения читающих сообщения пользователя и читаемых им пользователей будут напоминать гибрид страницы профиля пользователя и страницы со списком пользователей (Раздел 9.3.1), с сайдбаром пользовательской информации (включая статистику слежения за сообщениями) и таблицу пользователей. Кроме того, мы включим сетку пользовательских профильных изображений-ссылок в сайдбаре. Набросок соответствующий этим требованиям представлен на рис 11.14 (читаемые) и рис 11.15 (читатели).

    (рис 11.14) Набросок страницы со списком читаемых пользователей. (рис 11.15) Набросок страницы со списком читателей.

    Нашим первым шагом будет получение рабочих ссылок following (читаемые) и followers (читатели). Мы будем следовать примеру Твиттера и обе страницы будут требовать входа пользователя, как было протестировано в Листинге 11.28. Для вошедших пользователей, страницы должны иметь ссылки на читаемых и читателей, соответственно, как протестировано в Листинге 11.29.

    require 'spec_helper'
    
    describe "Authentication" do
      .
      .
      .
      describe "authorization" do
    
        describe "for non-signed-in users" do
          let(:user) { FactoryGirl.create(:user) }
    
          describe "in the Users controller" do
            .
            .
            .
            describe "visiting the following page" do
              before { visit following_user_path(user) }
              it { should have_title('Sign in') }
            end
    
            describe "visiting the followers page" do
              before { visit followers_user_path(user) }
              it { should have_title('Sign in') }
            end
          end
          .
          .
          .
        end
        .
        .
        .
      end
      .
      .
      .
    end
    require 'spec_helper'
    
    describe "User pages" do
      .
      .
      .
      describe "following/followers" do
        let(:user) { FactoryGirl.create(:user) }
        let(:other_user) { FactoryGirl.create(:user) }
        before { user.follow!(other_user) }
    
        describe "followed users" do
          before do
            sign_in user
            visit following_user_path(user)
          end
    
          it { should have_title(full_title('Following')) }
          it { should have_selector('h3', text: 'Following') }
          it { should have_link(other_user.name, href: user_path(other_user)) }
        end
    
        describe "followers" do
          before do
            sign_in other_user
            visit followers_user_path(other_user)
          end
    
          it { should have_title(full_title('Followers')) }
          it { should have_selector('h3', text: 'Followers') }
          it { should have_link(user.name, href: user_path(user)) }
        end
      end
    end

    Единственно сложная часть реализации это осуществление потребности добавления двух новых действий к контроллеру Users; основанных на маршрутах, определенных в Листинге 11.18, нам нужно назвать их following и followers. Каждому действию нужно установить заголовок, найти пользователя, вытянуть @user.followed_users или @user.followers (в пагинированной форме), а затем отренедерить страницу. Результат представлен в Листинге 11.30.

    class UsersController < ApplicationController
      before_action :signed_in_user,
                    only: [:index, :edit, :update, :destroy, :following, :followers]
      .
      .
      .
      def following
        @title = "Following"
        @user = User.find(params[:id])
        @users = @user.followed_users.paginate(page: params[:page])
        render 'show_follow'
      end
    
      def followers
        @title = "Followers"
        @user = User.find(params[:id])
        @users = @user.followers.paginate(page: params[:page])
        render 'show_follow'
      end
    
      private
      .
      .
      .
    end

    Отметим здесь, что оба действия делают явный вызов render, в данном случае делая рендеринг представления, названного show_follow, которое мы должны создать. Причина создания общего представления в том, что ERb является практически идентичным для обоих случаев, и Листинг 11.31 охватывает их.

    <% provide(:title, @title) %>
    <div class="row">
      <aside class="span4">
        <section>
          <%= gravatar_for @user %>
          <h1><%= @user.name %></h1>
          <span><%= link_to "view my profile", @user %></span>
          <span><b>Microposts:</b> <%= @user.microposts.count %></span>
        </section>
        <section>
          <%= render 'shared/stats' %>
          <% if @users.any? %>
            <div class="user_avatars">
              <% @users.each do |user| %>
                <%= link_to gravatar_for(user, size: 30), user %>
              <% end %>
            </div>
          <% end %>
        </section>
      </aside>
      <div class="span8">
        <h3><%= @title %></h3>
        <% if @users.any? %>
          <ul class="users">
            <%= render @users %>
          </ul>
          <%= will_paginate %>
        <% end %>
      </div>
    </div>

    С этим тесты должны пройти, а страницы должны выглядеть как на рис 11.16(читаемые) и рис 11.17(читатели).

    (рис 11.16) Отображение пользователей, читаемых текущим пользователем. (рис 11.17) Отображение читателей текущего пользователя.

    Стандартный способ реализации кнопки "читать" (follow)

    Теперь, когда наши представления в порядке, пришло время получить рабочие follow/unfollow кнопки. Тесты для этих кнопок комбинируют множество техник тестирования о которых было рассказано в этом учебнике и представляют из себя хорошее упражнение в чтении кода. Изучайте Листинг 11.32 до тех пор пока не убедитесь что вы понимаете что и почему мы тестируем. (Листинг 11.32 содержит одно небольшое упущение в безопасности; посмотрим, сможете ли вы выявить его. Мы скоро расскажем о нем.) Особенно обратите внимание на использование метода have_xpath использующего продвинутую и мощную технику XPath для навигации по XML документам (включая HTML5). Вы можете узнать больше о XPath с помощью поискового запроса XPath syntax.

    require 'spec_helper'
    
    describe "User pages" do
      .
      .
      .
      describe "profile page" do
        let(:user) { FactoryGirl.create(:user) }
        .
        .
        .
        describe "follow/unfollow buttons" do
          let(:other_user) { FactoryGirl.create(:user) }
          before { sign_in user }
    
          describe "following a user" do
            before { visit user_path(other_user) }
    
            it "should increment the followed user count" do
              expect do
                click_button "Follow"
              end.to change(user.followed_users, :count).by(1)
            end
    
            it "should increment the other user's followers count" do
              expect do
                click_button "Follow"
              end.to change(other_user.followers, :count).by(1)
            end
    
            describe "toggling the button" do
              before { click_button "Follow" }
              it { should have_xpath("//input[@value='Unfollow']") }
            end
          end
    
          describe "unfollowing a user" do
            before do
              user.follow!(other_user)
              visit user_path(other_user)
            end
    
            it "should decrement the followed user count" do
              expect do
                click_button "Unfollow"
              end.to change(user.followed_users, :count).by(-1)
            end
    
            it "should decrement the other user's followers count" do
              expect do
                click_button "Unfollow"
              end.to change(other_user.followers, :count).by(-1)
            end
    
            describe "toggling the button" do
              before { click_button "Unfollow" }
              it { should have_xpath("//input[@value='Follow']") }
            end
          end
        end
      end
      .
      .
      .
    end

    Листинг 11.32 тестирует кнопки кликая по ним и проверяя соответствующее поведение. Написание реализации подразумевает чуть более глубокое погружения в тему: following и unfollowing включает создание и уничтожение взаимоотношений, что означает необходимость определения create и destroy действий в контроллере Relationships (который нам еще нужно создать). Хотя кнопки появляются только для вошедших пользователей, что дает нам безопасность верхнего уровня, тесты в Листинге 11.32 упускают из виду безопасность на уровне контроллера, а именно: сами create и destroy должны быть доступны только для вошедших пользователей. (Это та самая уязвимость о которой мы говорили выше.) Листинг 11.33 выражает эти требования с помощью post и delete методов вызывающих эти действия напрямую.

    require 'spec_helper'
    
    describe "Authentication" do
      .
      .
      .
      describe "authorization" do
    
        describe "for non-signed-in users" do
          let(:user) { FactoryGirl.create(:user) }
          .
          .
          .
          describe "in the Relationships controller" do
            describe "submitting to the create action" do
              before { post relationships_path }
              specify { expect(response).to redirect_to(signin_path) }
            end
    
            describe "submitting to the destroy action" do
              before { delete relationship_path(1) }
              specify { expect(response).to redirect_to(signin_path) }
            end
          end
          .
          .
          .
        end
      end
    end

    Обратите внимание, что, для того чтобы избежать лишней работы по созданию практически бесполезного объекта Relationship, тест delete хардкодит id 1 в именованном маршруте:

    before { delete relationship_path(1) }

    Это работает из-за того что пользователь должен быть перенаправлен прежде чем приложение даже попытается обратиться к взаимоотношению с этим id.

    Код контроллера, необходимый для прохождения этих тестов удивительно краток: мы просто вытягиваем читаемого или читающего пользователя, а затем читаем или не читаем его сообщения используя соответствующий служебный метод. Полная реализация представлена в Листинге 11.34.

    class RelationshipsController < ApplicationController
      before_action :signed_in_user
    
      def create
        @user = User.find(params[:relationship][:followed_id])
        current_user.follow!(@user)
        redirect_to @user
      end
    
      def destroy
        @user = Relationship.find(params[:id]).followed
        current_user.unfollow!(@user)
        redirect_to @user
      end
    end

    В Листинге 11.34 мы можем видеть почему уязвимость отмеченная выше является незначительной: если невошедший пользователь попробовал бы обратиться к любому из действий напрямую (например, с помощью инструмента командной строки вроде curl), current_user был бы nil и в обоих случаях вторая строка действий вызвала бы исключение, что привело бы к ошибке, но не нанесло бы вреда приложению или его данным. Однако лучше на это не полагаться, так что мы предприняли дополнительный шаг и добавили дополнительный уровень безопасности.

    С этим ядро функциональности follow/unfollow завершено, и любой пользователь может читать (или не читать) сообщения любого другого пользователя, что вам стоит проверить и в вашем браузере и запустив набор тестов:

    $ bundle exec rspec spec/

    Реализация кнопки "читать" (follow) с Ajax

    Хотя наша реализация слежения за сообщениями пользователей является законченной и в своем нынешнем виде, нам осталось совсем немного подправить ее прежде чем заняться потоком сообщений. Вы могли заметить в Разделе 11.2.4 что оба create и destroy действия в контроллере Relationships просто перенаправляют обратно к исходному профилю. Другими словами, пользователь начинает на странице профиля, подписывается на сообщения пользователя и немедленно перенаправляется на исходную страницу. Резонный вопрос - почему пользователь вообще должен покидать эту страницу?

    Именно эту проблему решает Ajax, который позволяет веб страницам отправлять асинхронные запросы на сервер не покидая страницы.Так как номинально это является акронимом asynchronous JavaScript and XML, Ajax иногда ошибочно пишут как "AJAX", хотя на протяжении всей оригинальной Ajax статьи используется написание "Ajax". Поскольку практика добавления Ajax в веб формы является довольно распространенной, Rails делает реализацию Ajax легкой. Действительно, обновление партиалов формы follow/unfollow тривиально: просто заменим

    form_for

    на

    form_for ..., remote: true

    и Rails автомагически будет использовать Ajax.Это работает только если JavaScript включен в браузере, но изящно деградирует, работая в точности как в Разделе 11.2.4 если JavaScript отключен. Обновленные партиалы представлены в Листинге 11.35 и Листинге 11.36.

    <%= form_for(current_user.relationships.build(followed_id: @user.id),
                 remote: true) do |f| %>
      <div><%= f.hidden_field :followed_id %></div>
      <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
    <% end %>
    <%= form_for(current_user.relationships.find_by(followed_id: @user),
                 html: { method: :delete },
                 remote: true) do |f| %>
      <%= f.submit "Unfollow", class: "btn btn-large" %>
    <% end %>

    HTML сгенерированный этим ERb не особенно относится к делу, но вам может быть любопытно, так что взгляните:

    <form action="/relationships/117" class="edit_relationship" data-remote="true"
          id="edit_relationship_117" method="post">
      .
      .
      .
    </form>

    Это устанавливает переменную data-remote="true" внутри тега формы, что говорит Rails о том, что форма будет обрабатываться JavaScript. Используя простое свойство HTML вместо вставки полного JavaScript кода (как в предыдущих версиях Rails), Rails 3 следует философии ненавязчивого JavaScript.

    После обновления формы нам нужно уговорить контроллер Relationships отвечать на Ajax запросы. Тестирование Ajax является довольно сложным, и тщательное делание этого является большой темой со своими собственными правилами, но мы можем начать с кодом в Листинге 11.37. Это использует xhr метод (от "XmlHttpRequest") для выдачи Ajax запроса; сравните с get, post, patch и delete методами в предыдущих тестах. Затем мы проверяем что create и destroy действия делают правильные вещи когда вызываются Ajax запросом. (Для написания более основательного набора тестов для насыщенных Ajax-ом приложений, взгляните на Selenium и Watir.)

    require 'spec_helper'
    
    describe RelationshipsController do
    
      let(:user) { FactoryGirl.create(:user) }
      let(:other_user) { FactoryGirl.create(:user) }
    
      before { sign_in user, no_capybara: true }
    
      describe "creating a relationship with Ajax" do
    
        it "should increment the Relationship count" do
          expect do
            xhr :post, :create, relationship: { followed_id: other_user.id }
          end.to change(Relationship, :count).by(1)
        end
    
        it "should respond with success" do
          xhr :post, :create, relationship: { followed_id: other_user.id }
          expect(response).to be_success
        end
      end
    
      describe "destroying a relationship with Ajax" do
    
        before { user.follow!(other_user) }
        let(:relationship) { user.relationships.find_by(followed_id: other_user) }
    
        it "should decrement the Relationship count" do
          expect do
            xhr :delete, :destroy, id: relationship.id
          end.to change(Relationship, :count).by(-1)
        end
    
        it "should respond with success" do
          xhr :delete, :destroy, id: relationship.id
          expect(response).to be_success
        end
      end
    end

    Код в Листинге 11.37 это наш первый пример тестов контроллера, которыми я ранее интенсивно пользовался (например в предыдущем издании этого учебника), но которым я сейчас предпочитаю интеграционные тесты. Однако в данном случае, метод xhr (по непонятным мне причинам) не доступен в интеграционных тестах. Хотя мы впервые используем xhr, в этой точке учебника вы, вероятно, уже можете понять из контекста чем занимается этот код:

    xhr :post, :create, relationship: { followed_id: other_user.id }

    Мы видим что xhr принимает в качестве аргумента символ для соответствующего метода HTTP, символ для действия и хэш представляющий собой содержимое params в самом контроллере. Как и в предыдущих примерах, мы используем expect для того чтобы обернуть операцию в блок и протестировать увеличение или уменьшение соответсвующего количества.

    Как следует из тестов, код приложения использует те же create и delete действия для ответа на Ajax запросы которые он выдает чтобы ответить на обычные POST и DELETE HTTP запросы. Все что нам нужно сделать это ответить на обычный HTTP запрос с переадресацией (как в Разделе 11.2.4) и ответить на Ajax запрос с JavaScript. Код контроллера представлен в Листинге 11.38. (См. в Разделе 11.5 пример показывающий более компактный способ выполнить то же самое.)

    class RelationshipsController < ApplicationController
      before_action :signed_in_user
    
      def create
        @user = User.find(params[:relationship][:followed_id])
        current_user.follow!(@user)
        respond_to do |format|
          format.html { redirect_to @user }
          format.js
        end
      end
    
      def destroy
        @user = Relationship.find(params[:id]).followed
        current_user.unfollow!(@user)
        respond_to do |format|
          format.html { redirect_to @user }
          format.js
        end
      end
    end

    Этот код использует respond_to чтобы принять соответствующее действие в зависимости от вида запроса. (Это respond_to никак не связано с respond_to используемым в примерах RSpec.) Синтаксис может запутать и важно понимать, что в

    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end	

    выполняется только одна из строк (в зависимости от характера запроса).

    В случае Ajax запроса, Rails автоматически вызывает JavaScript Embedded Ruby (.js.erb) файл с тем же именем что и действие, т.е., create.js.erb или destroy.js.erb. Как вы можете догадаться, эти файлы позволяют смешивать JavaScript и Embedded Ruby для выполнения действий на текущей странице. Именно эти файлы нам нужны для создания и редактирования страницы профиля пользователя после начала слежения за сообщениями пользователя или после его прекращения.

    Внутри JS-ERb файла, Rails автоматически обеспечивает jQuery JavaScript хелперы для манипуляции страницей при помощи Document Object Model (DOM). Библиотека jQuery предоставляет большое количество методов для манипуляции DOM, но здесь нам понадобятся только два. Во первых мы должны знать о синтаксисе знака доллара, используемого для доступа к DOM элементу опираясь на его уникальный CSS id. Например, для манипуляции элементом follow_form, мы используем синтаксис

    $("#follow_form")

    (Вспомните из Листинг 11.23 что это div который обертывает форму, а не сама форма.) Этот синтаксис, вдохновленный CSS, использует символ # для указания CSS id. Как вы можете догадаться, jQuery, как и CSS, использует точку . для манипуляций с CSS классами.

    Второй метод который нам потребуется это html, который обновляет HTML внутри соответствующего элемента содержимым своего аргумента. Например, чтобы полностью заменить follow form на строку "foobar", мы можем написать

    $("#follow_form").html("foobar")

    В отличие от простых JavaScript файлов, JS-ERb файлы позволяют также использовать Embedded Ruby, что мы применяем в create.js.erb файле для замены формы на партиал unfollow (это то, что должно быть видно после успешной подписки на сообщения пользователя) и обновления количества читаемых. Результат представлен в Листинг 11.39. Здесь используется функция escape_javascript, которая необходима для маскирования результатов при вставке HTML в файл JavaScript.

    $("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
    $("#followers").html('<%= @user.followers.count %>')

    Файл destroy.js.erb аналогичен (Листинг 11.40).

    $("#follow_form").html("<%= escape_javascript(render('users/follow')) %>")
    $("#followers").html('<%= @user.followers.count %>')

    Теперь вам следует перейти на страницу профиля пользователя и проверить, что вы можете следить и не следить за сообщениями пользователей без обновления страницы, а набор тестов должен позеленеть:

    $ bundle exec rspec spec/

    Использование Ajax в Rails является большой и стремительно развивающейся темой, и здесь мы лишь слегка коснулись ее, но (как и с остальными материалами в этом учебнике) наш подход дает вам хорошую основу для изучения более продвинутых ресурсов.

    Поток сообщений

    Мы подошли к кульминации нашего примера приложения: потоку сообщений. Соответствено, этот раздел содержит некоторые из самых продвинутых материалов в данном учебнике. Полноценный поток сообщений опирается на свой прототип из Раздела 10.3.3 и собирает массив микросообщений из микросообщений читаемых пользователей, совместно с собственными микросообщениями текущего пользователя. Чтобы совершить этот подвиг, нам понадобятся некоторые довольно продвинутые Rails, Ruby и даже SQL техники программирования.

    Так как нам предстоит тяжелая работа, особенно важно понимать куда мы будем двигаться. Резюме окончательного потока сообщений пользователя, показанный на рис 11.5, вновь показан на рис 11.18.

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

    Мотивация и стратегия

    Основная идея потока сообщений проста. рис 11.19 показывает пример таблицы базы данных microposts и результирующий поток сообщений. Цель потока заключается в вытягивании микросообщений чей user id соответствует пользователям, сообщения которых читает текущий пользователь (и id самого текущего пользователя), как указано стрелками на схеме.

    (рис 11.19) Поток сообщений для пользователя (id 1) читающего сообщения пользователей 2, 7, 8 и 10.

    Поскольку нам необходим способ найти все микросообщения пользователей, за которыми следит данный пользователь, мы запланируем реализацию метода называемого from_users_followed_by, который мы будем использовать следующим образом:

    Micropost.from_users_followed_by(user)

    Хотя мы пока не знаем как реализовать его, мы уже можем написать тесты для его функциональности. Ключевым моментом является проверка всех трех требований к потоку: микросообщения читаемых пользователей и самого пользователя должны быт включены в поток, но в него не должны попадать сообщения пользователей, от чтения которых пользователь отписался. Первые два требования уже представлены в наших тестах: Листинг 10.35 проверяет что собственные микросообщения пользователя представлены в потоке, в то время как микросообщений отписанных пользователей быть не должно. Теперь, когда мы знаем как следить за сообщениями пользователей, мы можем добавить третий тип тестов, в этот раз проверяющих что микросообщения читаемых пользователей представлены в потоке, как это показано в Листинге 11.41.

    require 'spec_helper'
    
    describe User do
      .
      .
      .
      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
          let(:followed_user) { FactoryGirl.create(:user) }
    
          before do
            @user.follow!(followed_user)
            3.times { followed_user.microposts.create!(content: "Lorem ipsum") }
          end
    
          its(:feed) { should include(newer_micropost) }
          its(:feed) { should include(older_micropost) }
          its(:feed) { should_not include(unfollowed_post) }
          its(:feed) do
            followed_user.microposts.each do |micropost|
              should include(micropost)
            end
          end
        end
      end
      .
      .
      .
    end

    Сам поток сообщений просто перекладывает тяжелую работу на Micropost.from_users_followed_by, как это показано в Листинге 11.42.

    class User < ActiveRecord::Base
      .
      .
      .
      def feed
        Micropost.from_users_followed_by(self)
      end
      .
      .
      .
    end

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

    Пришло время реализовать Micropost.from_users_followed_by, который мы для простоты будем называть "поток". Поскольку конечный результат довольно сложен, мы будем строить итоговую реализацию потока по кусочкам.

    В первую очередь нужно подумать о том, какой вид запроса нам нужен. Что мы хотим сделать, это выбрать из таблицы microposts все микросообщения с id соответствующими пользователям, читаемыми данным пользователем (или самому пользователю). Мы можем схематически написать это следующим образом:

    SELECT * FROM microposts
    WHERE user_id IN (<list of ids>) OR user_id = <user id>

    При написании этого кода мы предположили, что SQL поддерживает ключевое слово IN, что позволяет нам протестировать множественное включение. (К счастью это так.)

    Вспомним из предварительной реализации потока в Разделе 10.3.3 что Active Record использует where метод для осуществления вида выбора, показанного выше, что иллюстрирует Листинг 10.36. Там наша выборка была очень простой; мы просто взяли все микросообщения с user id соответствующим текущему пользователю:

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

    Здесь мы ожидаем, что он будет более сложным, чем то вроде

    where("user_id in (?) OR user_id = ?", following_ids, user)

    (Здесь мы в состоянии использовать, согласно Rails конвенции, user вместо user.id; Rails автоматически использует id. Мы также опустили впереди идущую часть Micropost. поскольку мы ожидаем что этот метод будет жить в самой модели Micropost.)

    Мы видим из этих условий, что нам нужен массив id пользователей, читаемых данным пользователем (или какой-то эквивалент). Один из способов сделать это заключается в использовании Ruby метода map, доступного на любом "перечисляемом" объекте, т.е., любом объекте (таком как Массив или Хэш), который состоит из коллекции элементов.Основное требование заключается в том, что перечисляемые объекты должны реализовывать each метод для перебора коллекции. Мы видели пример этого метода в Разделе 4.3.2; он работает следующим образом:

    $ rails console
    >> [1, 2, 3, 4].map { |i| i.to_s }
    => ["1", "2", "3", "4"]

    Ситуации, подобные той, что показана выше, где такой же метод (например, На самом деле такая нотация на самом деле изначально была расширением которое Rails вносил в ядро ??языка Ruby; она была настолько полезной, что в настоящее время она включена в сам Ruby. Замечательно, правда?

    >> [1, 2, 3, 4].map(:to_s)
    => ["1", "2", "3", "4"]

    Используя метод join (Раздел 4.3.1), мы можем создать строку состоящую из id объединив их через запятую-пробел:

    >> [1, 2, 3, 4].map(:to_s).join(', ')
    => "1, 2, 3, 4"

    Мы можем использовать вышеприведенный метод для построения необходимого массива id читаемых пользователей вызвав id на каждом элементе в user.followed_users. Например, для первого пользователя в базе данных этот массив выглядит следующим образом:

    >> User.first.followed_users.map(:id)
    => [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
    24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
    43, 44, 45, 46, 47, 48, 49, 50, 51]

    Фактически, так как конструкции такого вида очень полезны, Active Record обеспечивает ее по умолчанию:

    >> User.first.followed_user_ids
    => [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
    24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
    43, 44, 45, 46, 47, 48, 49, 50, 51]

    Здесь метод followed_user_ids синтезирован библиотекой Active Record на основе ассоциации has_many :followed_users (Листинг 11.10); в результате, для получения id соответствующих коллекции user.followed_users, нам достаточно добавить _ids к названию ассоциации. Строка id читаемых пользователей тогда будет выглядеть следующим образом:

    >> User.first.followed_user_ids.join(', ')
    => "4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
    24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
    43, 44, 45, 46, 47, 48, 49, 50, 51"

    Однако при вставке строки в SQL, вам нет надобности делать этого; интерполяция ? заботится об этом за вас (и фактически устраняет некоторые несовместимости связанные с базой данных). Это означает что мы можем использовать

    user.followed_user_ids

    само по себе.

    В этой точке вы можете догадаться что код вроде

    Micropost.from_users_followed_by(user)

    будет включать в себя метод класса в Micropost классе (конструкция кратко упоминавшаяся в Разделе 4.4.1). Предполагаемая реализация с этими строками представлена в Листинге 11.43.

    class Micropost < ActiveRecord::Base
      .
      .
      .
      def self.from_users_followed_by(user)
        followed_user_ids = user.followed_user_ids
        where("user_id IN (?) OR user_id = ?", followed_user_ids, user)
      end
    end

    Хотя обсуждение ведущее к Листингу 11.43 было выдержано в гипотетических тонах, он действительно работает! Вы можете проверить это запустив набор тестов, которые должны пройти:

    $ bundle exec rspec spec/

    В большинстве приложений этой начальной реализации было бы вполне достаточно для большинства практических целей. Но это не финальная реализация; посмотрим, сможете ли вы догадаться почему, прежде чем перейти к следующему разделу. (Намек: А что если пользователь следит за сообщениями 5000 других пользователей?)

    Подзапросы

    Как намекалось в последнем разделе, реализация потока сообщений в Section 11.3.2 не очень хорошо масштабируется при большом количестве микросообщений, что скорее всего произойдет если пользователь начнет читать сообщения, скажем, 5000 других пользователей. В этом разделе мы повторно реализуем ленту сообщений способом, который лучше масштабируется с количеством читаемых пользователей.

    Проблема с кодом из Раздела 11.3.2 в том что

    followed_user_ids = user.followed_user_ids

    вытягивает всех читаемых пользователей в память и создает массив длинной во весь список читаемых пользователей. Поскольку условие в Листинге 11.43 на самом деле лишь проверяет включение во множество, должен быть более эффективный способ для этого, да и SQL оптимизирован именно для таких множественных операций. Решение заключается в отправке поиска id читаемых пользователей в базу данных с помощью подзапроса.

    Мы начнем с рефакторинга потока немного модифицированным кодом в Листинге 11.44.

    class Micropost < ActiveRecord::Base
      .
      .
      .
      # Returns microposts from the users being followed by the given user.
      def self.from_users_followed_by(user)
        followed_user_ids = user.followed_user_ids
        where("user_id IN (:followed_user_ids) OR user_id = :user_id",
              followed_user_ids: followed_user_ids, user_id: user)
      end
    end

    В качестве подготовки к следующему шагу мы заменили

    where("user_id IN (?) OR user_id = ?", followed_user_ids, user)

    на эквивалентное

    where("user_id IN (:followed_user_ids) OR user_id = :user_id",
          followed_user_ids: followed_user_ids, user_id: user)

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

    Обсуждение выше привело нас к тому что мы добавим второе вхождение user_id в SQL запросе. В частности, мы можем заменить Ruby код

    followed_user_ids = user.followed_user_ids

    на фрагмент SQL

    followed_user_ids = "SELECT followed_id FROM relationships
                         WHERE follower_id = :user_id"

    Этот код содержит подзапрос SQL и внутренне вся выборка для пользователя 1 будет выглядеть примерно так:

    SELECT * FROM microposts
    WHERE user_id IN (SELECT followed_id FROM relationships
                      WHERE follower_id = 1)
          OR user_id = 1

    Этот подзапрос организует всю логику для отправки в базу данных, что является более эффективным.Для более продвинутых способов создания необходимых подзапросов, см. сообщение в блоге "Hacking a subselect in ActiveRecord".

    С этим фундаментом мы готовы к эффективной релизации потока сообщений, как видно в Листинге 11.45. Обратите внимание, что, так как теперь это чистый SQL, followed_user_ids является интерполированным, а не маскированным. (На самом деле рабочими являются оба варианта, но мне кажется более логичным интерполировать в данном контексте.)

    class Micropost < ActiveRecord::Base
      belongs_to :user
      default_scope -> { order('created_at DESC') }
      validates :content, presence: true, length: { maximum: 140 }
      validates :user_id, presence: true
    
      # Returns microposts from the users being followed by the given user.
      def self.from_users_followed_by(user)
        followed_user_ids = "SELECT followed_id FROM relationships
                             WHERE follower_id = :user_id"
        where("user_id IN (#{followed_user_ids}) OR user_id = :user_id",
              user_id: user.id)
      end
    end

    Этот код представляет собой внушительную комбинацию Rails, Ruby, и SQL, но он делает свою работу и делает ее хорошо. (Конечно же, даже подзапрос не является универсальным решением для масштабирования. Для бОльших сайтов, вам, вероятно, потребуется генерировать поток асинхронно с помощью фонового процесса. Такие тонкости масштабирования выходят за рамки данного руководства.)

    Новый поток сообщений

    С кодом в Листинге 11.45, наш поток сообщений завершен. Напомним, что код для Home страницы, представлен в Листинге 11.46; этот код создает пагинированный поток соответствущих микросообщений для использования в представлении, как видно в рис 11.45" type="example">Листинге 11.45, наш поток сообщений завершен. Напомним, что код для Home страницы, представлен в Листинге 11.46; этот код создает пагинированный поток соответствущих микросообщений для использования в представлении, как видно в Для того чтобы сделать поток сообщений на рис 11.20 более привлекательным, я добавил несколько дополнительных микросообщений вручную используя Rails консоль. Отметим, что paginate метод фактически достигает цели в методе модели Micropost в Листинге 11.45, организуя вытягивание только 30 микросообщений за раз из базы данных. (Вы можете проверить это изучив SQL выражения в логах сервера разработки.)

    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
    (рис 11.20) Home страница с работающим потоком сообщений.

    Заключение

    Добавив ленту сообщений, мы закончили ключевой пример приложения Учебника Ruby on Rails. Это приложение включает в себя примеры всех основных возможностей Rails, включая модели, представления, контроллеры, шаблоны, партиалы, фильтры, валидации, обратные вызовы, has_many/belongs_to и has_many through ассоциации, безопасность, тестирование и развертывание. Несмотря на этот внушительный список, вам предстоит еще очень многое узнать о Rails. В качестве первого шага на этом пути, этот раздел содержит некоторые рекомендуемые расширения основного приложения, а также рекомендации для дальнейшего обучения.

    Прежде чем перейти к решению любого из предложенных расширений приложения, хорошо бы объединить ваши изменения:

    $ git add .
    $ git commit -m "Add user following"
    $ git checkout master
    $ git merge following-users

    Как обычно, если хотите, вы можете также отправить ваше приложение и развернуть его на сервере:

    $ git push
    $ git push heroku
    $ heroku pg:reset DATABASE
    $ heroku run rake db:migrate
    $ heroku run rake db:populate

    Расширения к примеру приложения

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

    Не удивляйтесь, если по началу будет непросто; чистый лист новой фичи может быть немного пугающим. Чтобы помочь вам начать, я могу дать две большие рекомендации. Во-первых, пежде чем добавлять какую-либо функцию к Rails приложению, взгляните на RailsCasts archive чтобы посмотреть, не рассказал ли уже об этом Ryan Bates.Единственная моя оговорка по поводу Railscasts — они обычно опускают тесты. Это, вероятно, необходимо для сохранения красоты и краткости эпизодов, но у вас может сформироваться неправильное представление о важности тестов. После просмотра соответствующего Railscast для получения представления о процессе, я советую писать новую фнкцию используя разработку через тестирование. (В этом контексте я рекомендую взглянуть на the RailsCast on "How I test". Вы увидите, что Ryan Bates сам обычно использует TDD для разработки в реальной жизни и что, фактически, его стиль тестирования совпадает со стилем используемым в данном учебнике.) Если он это сделал, просмотр соответствующего Railscast сэкономит вам массу времени. Во-вторых, всегда делайте обширный поиск в Google по вашей предполагаемой функции, чтобы найти соответствующее сообщения в блогах и пособиях. Разработка веб приложений это непростое дело и это поможет вам учиться на чужом опыте (и ошибках).

    Многие из следующих функций являются довольно непростыми задачами, и я дал несколько подсказок по поводу средств, которые могут вам понадобиться для их реализации. Даже с подсказками, они являются намного более трудными чем упражнения, которые приводились в конце каждой главы учебника, так что не расстраивайтесь, если вы не можете решить их без значительных усилий. Из-за нехватки времени я недоступен для личной помощи, но если есть достаточный интерес я мог бы выпустить автономную статью/скринкаст охватывающий эти расширения в будущем; перейдите на основной сайт Rails Tutorial http://railstutorial.org/ и подпишитесь на ленту новостей, чтобы быть в курсе последних обновлений.

    Реплики

    Твиттер позволяет пользователям делать "@replies", которые являются микросообщениями, чьи первые символы являются логином пользователя предшествующим знаку @. Эти сообщения появляются только в потоке сообщений у пользователя задавшего вопрос или у пользователей читающих данного пользователя. Реализуйте упрощенную версию этого, ограничив появление @replies только в потоках сообщений получателя и отправителя. Это может подразумевать добавление in_reply_to столбца в таблицу microposts и дополнительного including_replies пространства к модели Micropost

    Поскольку нашему приложению не хватает уникальных пользовательских логинов, вам также необходимо решить, каким способом представлять пользователей. Один из вариантов это использование комбинации id и имени, например @1-michael-hartl. Другой способ это добавить уникальное имя пользователя в процесс регистрации и затем использовать его в @replies.

    Обмен сообщениями

    Твиттер поддерживает непосредственный (приватный) обмен сообщениями с помощью добавления префикса с буквой "d" к микросообщению. Реализуйте эту функцию для примера приложения. Решение, вероятно, подразумевает наличие модели Message и проверку новых микросообщений с помощью регулярных выражений.

    Уведомления о новых читателях

    Реализуйте функцию, отправляющую каждому пользователю email уведомление когда у него появляется новый читатель. Затем сделайте уведомления необязательными, так чтобы пользователи могли отказаться при желании. Помимо всего прочего, добавление этой функции требует знания о том, как отправлять почту с помощью Rails. Начните с RailsCast on Action Mailer in Rails 3.

    Напоминание пароля

    В настоящее время, если пользователи нашего приложения забудут свои пароли, они не смогут их восстановить. Из-за одностороннего безопасного хэширования паролей в Главе 6, наше приложение не может отправить по email пароли пользователей, но оно может отправить ссылку на форму сброса пароля. Используя RailsCast on Remember Me Reset Password в качестве примера, исправьте это упущение.

    Подтверждение регистрации

    Помимо регулярного выражения для электронной почты, пример приложения в настоящее время не имеет способа проверки валидности пользовательского email адреса. Добавьте шаг проверки email адреса в подтверждение регистрации пользователя. Новая функция должна создавать пользователей в неактивном состоянии, отправлять по email пользователям активационный URL, а затем активировать статус пользователя при посещении соответствующего URL. Для работы с активный/неактивный переходами вам может помочь прочтение state machines in Rails.

    RSS канал

    Реализовать для каждого пользователя RSS канал их микросообщений. Затем реализовать RSS канал для их лент сообщений, опционально ограничив доступ к этому каналу используя аутентификационную схему. RailsCast on generating RSS feeds поможет вам начать.

    REST API

    Многие веб сайты раскрывают Application Programmer Interface (API) так что сторонние приложения могут get, post, put и delete ресурсы приложения. Реализуйте такой REST API для примера приложения. Решение подразумевает добавление respond_to блоков (Раздел 11.2.5) ко многим действиям Application контроллера; они должны отвечать на запросы для XML. Позаботьтесь о безопасности; API должен быть доступен только авторизированным пользователям.

    Поиск

    В настоящее время у пользователей нет другого способа найти друг-друга, кроме как просмотром списка пользователей или просматривая потоки сообщений других пользователей. Реализуйте функцию поиска чтобы исправить ситуацию. Затем добавьте другую поисковую функцию для микросообщений. RailsCast on simple search forms поможет вам начать. Если вы используете шаред хостинг или выделенный сервер, я советую использовать Thinking Sphinx (см. RailsCast on Thinking Sphinx). Если вы развернуты на Heroku, вы должны следовать инструкциям Heroku full text search.

    Руководство по дальнейшим ресурсам

    Существует огромное количество Rails ресурсов в магазинах и в сети — предложений настолько много что это может ошеломить. Хорошие новости заключаются в том, что, если вы дошли до этого места учебника, вы готовы практически ко всему за его пределами. Вот несколько советов:

  • The Ruby on Rails Tutorial screencasts: я подготовил полноценный скринкаст курс основанный на этой книге. В дополнение к раскрытию всех материалов этой книги, скринкасты дополнены советами, трюками и демонстрациями типа смотри-как-это-делается, которые сложно зафиксировать в печатном варианте. Их можно приобрести через сайт Ruby on Rails Tutorial.
  • RailsCasts: трудно переоценить важность ресурса Railscasts, я советую начать с посещения архива эпизодов Railscasts и клика по любой зацепившей вас теме.
  • Ruby и Rails книги: я рекомендую Beginning Ruby Петера Купера, The Well-Grounded Rubyist David A. Black, Eloquent Ruby Russ Olsen и The Ruby Way Хэла Фултона для дальнейшего изучения Ruby, и The Rails 3 Way Оби Фернандеса, и Rails 3 in Action (подождите второго издания) Ryan Bigg и Yehuda Katz для изучения Rails.
  • PeepCode и Code School: скринкасты от PeepCode и интерактивные курсы в Code School неизменно высокого качества и я горячо рекомендую их.
  • Упражнения

    Добавьте тесты для уничтожения взаимоотношений связанных с данным пользователем (т.e., к реализации dependent :destroy в Листинге 11.4 и Листинге 11.16). Подсказка: следуйте примеру в Листинге 11.12.

    Метод respond_to виденый в Листинге 11.38 на самом деле может быть поднят из действий в сам контроллер Relationships, и respond_to блоки могут быть заменены на Rails метод respond_with. Подтвердите, что результирующий код, показаный в Листинге 11.47, является корректным, проверив что набор тестов все еще проходит. (Подробнее об этом методе см в Google поиске "rails respond_with".)

    Сделайте рефакторинг Листинга 11.31 добавив партиалы для кода общего для страниц following/followers, страницы Home, и страницы показывающей пользователя.

    Следуя модели в Листинге 11.19, напишите тесты для статистики на странице профиля.

    class RelationshipsController < ApplicationController
      before_action :signed_in_user
    
      respond_to :html, :js
    
      def create
        @user = User.find(params[:relationship][:followed_id])
        current_user.follow!(@user)
        respond_with @user
      end
    
      def destroy
        @user = Relationship.find(params[:id]).followed
        current_user.unfollow!(@user)
        respond_with @user
      end
    end
    Вернуться к учебному плану