Ruby on Rails Tutorial
Изучение Rails на Примерах
Майкл Хартл
Содержание
- Предисловие к русскому изданию
- Глава 1 От нуля к развертыванию
- Глава 2 demo app
- Глава 3 В основном статические страницы
- Глава 4 Rails-приправленный Ruby
- Глава 5 Заполнение шаблона
- Глава 6 Моделирование пользователей
- Chapter 7 Регистрация
- Глава 8 Войти, выйти
- Глава 9 Обновление, демонстрация и удаление пользователей
- Глава 10 Микросообщения пользователей
- Глава 11 Слежение за сообщениями пользователей
Предисловие
Моя компания (CD Baby) была одной из первых громко перешедших на Ruby on Rails, а затем еще громче вернувшейся обратно на PHP (Google расскажет вам об этой драме). Эту книгу, написанную Майклом Хартлом так высоко рекомендовали, что я должен был попробовать её, и Ruby on Rails Tutorial это всё, что я использовал, чтобы вернуться к Rails.
Хотя я уже прошел через много книг по Rails, это одна из немногих, что, наконец, зацепила меня. Было много написано книг типа «Путь Rails» — после которых я чувствовал себя неестественно, но после этой книги я наконец почувствовал себя естественно. Это также единственная книга по Rails, которая соблюдает методику «разработка через тестирование» на всем своем протяжении, этот подход строго рекомендуется специалистами, но он никогда не был так чётко продемонстрирован ранее. Наконец, Git, GitHub и Heroku присутствуют в демо-примерах, автор действительно дает вам почувствовать, что он хотел сделать реальный проект. Учебный код примеров не изолирован.
Линейное повествование — отличный формат. Лично я прошел Rails Tutorial в течении трёх долгих дней, делая все примеры и задачи в конце каждой главы. Делайте всё от начала до конца, не прыгая, и вы получите максимальную пользу.
Наслаждайтесь!
Derek Sivers (sivers.org)
Основатель CD Baby
Благодарности
Ruby On Rails Учебник во многом обязан моей предыдущей книге по Rails, RailsSpace и, следовательно, моему соавтору Aurelius Prochazka. Я хотел бы поблагодарить Aure как за работу, которую он проделал над прошлой книгой, так и за поддержку этой. Я также хотел бы поблагодарить Debra Williams Cauley, редактора обеих книг RailsSpace и Rails Tutorial; до тех пор, пока она не прекратит брать меня на бейсбол, я буду продолжать писать книги для нее.
Я хотел бы поблагодарить огромное количество Рубистов учивших и вдохновлявших меня на протяжении многих лет: David Heinemeier Hansson, Yehuda Katz, Carl Lerche, Jeremy Kemper, Xavier Noria, Ryan Bates, Geoffrey Grosenbach, Peter Cooper, Matt Aimonetti, Gregg Pollack, Wayne E. Seguin, Amy Hoy, Dave Chelimsky, Pat Maddox, Tom Preston-Werner, Chris Wanstrath, Chad Fowler, Josh Susser, Obie Fernandez, Ian McFarland, Steven Bristol, Pratik Naik, Sarah Mei, Sarah Allen, Wolfram Arnold, Alex Chaffee, Giles Bowkett, Evan Dorn, Long Nguyen, James Lindenbaum, Adam Wiggins, Tikhon Bernstam, Ron Evans, Wyatt Greene, Miles Forrest, хороших людей из Pivotal Labs, команду Heroku, thoughtbot ребят, и команду GitHub. Наконец, многих, многих читателей - слишком много чтобы перечислять их здесь - внёсших большое количество предложений по улучшению и сообщивших об ошибках во время написания этой книги, и я с благодарностью признаю их помощь в написании ее настолько хорошей, насколько это было возможно.
Об авторе
Майкл Хартл – автор Ruby on Rails Tutorial, лидирующего введения в веб разработку на Ruby on Rails. Его предыдущий опыт включает в себя написание и разработку RailsSpace - чрезвычайно устаревшего учебника по Rails и разработку Insoshi - некогда популярной, а ныне устаревшей платформы для социальных сетей написанной на Ruby on Rails. В 2011, Майкл получил Ruby Hero Award за его вклад в Ruby сообщество. Он закончил Harvard College, имеет степень Кандидата Физических Наук присвоенную в Caltech и является выпускником предпринимательских курсов Y Combinator.
Копирайт и лицензия
Ruby on Rails Tutorial: Learn Web Development with Rails. Copyright © 2012 by Michael Hartl. Весь исходный код в Ruby on Rails Tutorial доступен под MIT License и Beerware License.
Лицензия MIT
Copyright (c) 2013 Michael Hartl
Данная лицензия разрешает лицам, получившим копию данного программного
обеспечения и сопутствующей документации (в дальнейшем именуемыми
«Программное Обеспечение»), безвозмездно использовать Программное
Обеспечение без ограничений, включая неограниченное право на использование,
копирование, изменение, добавление, публикацию, распространение,
сублицензирование и/или продажу копий Программного Обеспечения, также
как и лицам, которым предоставляется данное Программное Обеспечение,
при соблюдении следующих условий:
Указанное выше уведомление об авторском праве и данные условия должны быть
включены во все копии или значимые части данного Программного Обеспечения.
ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО
ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ
ГАРАНТИЯМИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И
ОТСУТСТВИЯ НАРУШЕНИЙ ПРАВ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ
НЕСУТ ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ ИЛИ ДРУГИХ
ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ ИНОМУ, ВОЗНИКШИМ ИЗ,
ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ
ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
/*
* ----------------------------------------------------------------------------
* "ПИВНАЯ ЛИЦЕНЗИЯ" (Ревизия 42):
* Весь код написан Майклом Хартлом. До тех пор пока вы осознаете это,
* вы можете делать с ним все что захотите. Если мы когда нибудь
* встретимся, и если это того стоило, вы можете купить мне
* пиво в ответ.
* ----------------------------------------------------------------------------
*/
Глава 11 Слежение за сообщениями пользователей
В этой главе мы завершим ядро примера приложения, добавив социальный слой, что позволит пользователям читать (и не читать) сообщения других пользователей 17, в результате чего на главной странице каждого пользователя будет отображаться список микросообщений тех пользователей, сообщения которых он читает. Мы также сделаем представления для отображения читающих и читаемых пользователей. Мы узнаем, как смоделировать взаимоотношения между пользователями в Разделе 11.1, а затем сделаем веб-интерфейс в Разделе 11.2 (включая введение в Ajax). Наконец, мы закончим, разработав полнофункциональный поток сообщений в Разделе 11.3.
Эта последняя глава содержит несколько из наиболее сложных материалов учебника, в том числе, сложные модели данных и несколько Ruby / SQL хитростей для создания потока сообщений. С помощью этих примеров вы увидите как Rails может обрабатывать даже весьма сложные модели данных, что должно вам пригодиться, так как вы двигаетесь к разработке собственных приложений с их собственными требованиями. Чтобы помочь с переходом от учебника к самостоятельной разработке, Раздел 11.4 содержит рекомендуемые расширения к ядру примера приложения, а также ссылки на более продвинутые ресурсы.
Как обычно, Git пользователи должны создать новую тему ветки:
$ git checkout -b following-users
Так как материал этой главы особенно сложен, прежде чем писать код, мы улучим момент и сделаем небольшой обзор интерфейса. Как и в предыдущих главах, на этом раннем этапе мы будем представлять страницы используя наброски.1 Полная последовательность страниц работает следующим образом: пользователь, (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 Модель Relationship
Наш первый шаг в реализации слежения за сообщениями пользователей, заключается в построении модели данных, которая не так проста, как кажется. Naïvely, кажется, что has_many
отношение должно сработать: пользователь has_many
(имеет_много) читаемых и has_many
(имеет_много) читателей. Как мы увидим, в этом подходе есть проблема, и мы узнаем как ее исправить используя has_many through
. Вполне вероятно, что многие идеи этого раздела окажутся непонятыми с первого раза, и может потребоваться некоторое время для осознания довольно сложной модели данных. Если вы обнаружите что запутались, попробуйте пройти главу до конца, а затем прочитать этот раздел еще раз, чтобы прояснить для себя некоторые вещи.
11.1.1 Проблема с моделью данных (и ее решение)
В качестве первого шага на пути построения модели данных для слежения за сообщениями пользователей, давайте рассмотрим следующий типичный случай. Возьмем, в качестве примера, пользователя, который следит за сообщениями второго пользователя: мы могли бы сказать, что, например, Кальвин читает сообщения Гоббса, и Гоббс читается Кальвином, таким образом, Кальвин является читателем, а Гоббс является читаемым. При использовании дефолтной Rails’ плюрализации, множество таких читаемых пользователей называлось бы followers, и user.followers
был бы массивом таких пользователей. К сожалению, реверс не сработает: по умолчанию, множество всех читаемых пользователей называлось бы followeds, что является безграмотной неуклюжестью. Мы могли бы назвать их following, но это тоже сомнительная идея: в нормальном английском, “following” это множество людей, следящих за вами, т.e., ваши последователи — с точностью до наоборот от предполагаемого значения. Хотя мы будем использовать “following” в качестве метки, как в “50 following, 75 followers”, мы будем использовать “followed users” для самих пользователей, с соответствующим массивом user.followed_users
.2
Это предполагает моделирование читаемых пользователей как на Рис. 11.6, с followed_users
таблицей и has_many
ассоциацией. Поскольку user.followed_users
должно быть массивом пользователей, каждая строка таблицы followed_users
должна быть пользователем, идентифицируемым с помощью followed_id
, совместно с follower_id
для установления ассоциации.3 Кроме того, так как каждая строка является пользователем, мы должны были бы включить другие атрибуты пользователя, включая имя, пароль и т.д.
Проблема модели данных из Рис. 11.6 в том, что она ужасно избыточна: каждая строка содержит не только id каждого читаемого пользователя, но и всю остальную информацию, уже содержащуюся в таблице users
. Еще хуже то, что для моделирования читателей пользователя нам потребуется отдельная followers
таблица. Наконец, эта модель данных кошмарно неудобна в эксплуатации, так как каждый раз при изменении пользователем (скажем) своего имени, нам пришлось бы обновлять запись пользователя не только в users
таблице, но также каждую строку, содержащую этого пользователя в обоих followed_users
и followers
таблицах.
Проблема здесь в том, что нам не хватает лежащей в основе абстракции. Один из способов найти правильную абстракцию, это рассмотреть, как мы могли бы реализовать чтение сообщений пользователя в веб-приложении. Вспомним из Раздела 7.1.2, что REST архитектура включает в себя ресурсы которые создаются и уничтожаются. Это приводит нас к двум вопросам: Что создается, когда пользователь начинает читать сообщения другого пользователя? Что уничтожается, когда пользователь прекращает следить за сообщениями другого пользователя?
Поразмыслив, мы видим, что в этих случаях приложение должно создать либо разрушить взаимоотношение между двумя пользователями. Затем пользователь has_many :relationships
(имеет_много :взаимоотношений), и имеет много followed_users
(или followers
) через эти взаимоотношения. Действительно, Рис. 11.6 уже содержит бОльшую часть реализации: поскольку каждый читаемый пользователь уникально идентифицирован посредством 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
Этот код автоматически заполняет user.followed_users
массивом читаемых пользователей. Схема модели данных представлена на Рис. 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.
relationships
таблицы. db/migrate/[timestamp]_create_relationships.rb
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.19.) Как мы увидим в Разделе 11.1.4, наш пользовательский интерфейс не позволит этому случиться, но добавление индекса уникальности позволит избежать ошибки в случае, если пользователь попытается дублировать взаимотношения любым другим способом (используя, например, инструмент командной строки, такой как curl). Мы могли бы также добавить валидацию уникальности к модели Relationship, но, так как дублирование взаимоотношений является ошибкой всегда, для наших целей вполне достаточно индекса уникальности.
Для создания таблицы relationships
, мы мигрируем базу данных и подготавливаем тестовую бд, как обычно:
$ bundle exec rake db:migrate
$ bundle exec rake test:prepare
Результирующая модель данных Relationship показана на Рис. 11.8.
11.1.2 Ассоциации пользователь/взаимоотношение
Прежде чем приступить к реализации читателей и читаемых, нам вначале необходимо установить ассоциацию между пользователями и взаимоотношениями. Пользователь has_many
(имеет_много) взаимоотношений, и, так как взаимоотношения включают двух пользователей — взаимоотношение belongs_to
(принадлежит_к) читающим и читаемым пользователям.
Как и с микросообщениями в Разделе 10.1.3, мы будем создавать новые взаимоотношения используя ассоциацию, с помощью такого кода
user.relationships.build(followed_id: ...)
Мы начнем с базовых тестов валидации показанных в Листинге 11.2.
spec/models/relationship_spec.rb
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, которые использовали @user
и @micropost
, соответственно, Листинг 11.2 использует let
вместо переменных экземпляра. Отличия между ними редко имеют значение,4 но я считаю let
более чистым решением, нежели переменные экземпляра. Ранее мы применяли переменные экземпляра из-за того что нам важно было как можно раньше ввести переменные экземпляра, а также потому что let
является немного более продвинутой техникой.
Мы также должны протестировать атрибут relationships
модели User как это показано в Листинге 11.3.
user.relationships
. spec/models/user_spec.rb
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
поскольку у таблицы microposts
есть атрибут user_id
для идентификации пользователя (Раздел 10.1.1). Id используемый таким способом для связи двух таблиц базы данных, известен как внешний ключ, и когда внешним ключом для объекта модели User является user_id
, Rails может вывести ассоциацию автоматически: по умолчанию, Rails ожидает внешний ключ в форме <class>_id
, где <class>
является строчной версией имени класса.5 В данном случае, несмотря на то, что мы по прежнему имеем дело с пользователями, они теперь отождествляются с внешним ключом follower_id
, поэтому мы должны сообщить об этом Rails, как показано в Листинге 11.4.6
has_many
ассоциации пользователь/взаимоотношение. app/models/user.rb
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.
belongs_to
ассоциации пользователь/взаимоотношения. spec/models/relationship_spec.rb
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
.
belongs_to
ассоциаций к модели Relationship. app/models/relationship.rb
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/
11.1.3 Валидации
Прежде чем двигаться дальше, мы добавим пару валидаций модели Relationship для комплектности. Тесты (Листинг 11.7) и код приложения (Листинг 11.8) просты.
spec/models/relationship_spec.rb
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
app/models/relationship.rb
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
11.1.4 Читаемые пользователи
Теперь мы переходим к сердцу ассоциаций Relationship: followed_users
и followers
. Мы начнем с followed_users
, как показано в Листинге 11.9.
user.followed_users
. spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
it { should respond_to(:relationships) }
it { should respond_to(:followed_users) }
.
.
.
end
Реализация впервые использует has_many through
: пользователь имеет много читаемых (пользователей) через взаимоотношения, как показано на Рис. 11.7. По умолчанию, в ассоциации 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.
followed_users
.app/models/user.rb
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
Для того, чтобы создавать взаимоотношение с читаемым (пользователем), мы введем служебный метод follow!
с тем чтобы мы могли написать user.follow!(other_user)
. (Этот метод follow!
должен работать всегда, так что, как и с create!
и с save!
, мы обозначаем восклицательным знаком что при неудачном создании будет брошено исключение.) Мы также добавим связанный булев метод following?
для того чтобы иметь возможность проверять - читает ли пользователь сообщения других пользователей.7 Тесты в Листинге 11.11 показывают как мы планируем использовать эти методы на практике.
spec/models/user_spec.rb
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.
following?
и follow!
. app/models/user.rb
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.
spec/models/user_spec.rb
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).
app/models/user.rb
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
11.1.5 Читатели пользователя
Последней частью пазла взаимоотношений является метод user.followers
сопутствующий user.followed_users
. Вы могли заметить в Рис. 11.7 что все сведения, необходимые для извлечения массива читателей уже присутствуют в таблице relationships
. Действительно, техника та же, что и для читаемых пользователей, но с реверсированием ролей follower_id
и followed_id
. Это говорит о том, что, если бы мы смогли как-то организовать таблицу reverse_relationships
, поменяв местами эти два столбца (Рис. 11.9), то мы бы с легкостью реализовали user.followers
.
Начнем с тестов, веря, что магия Rails выручит нас (когда дело дойдет до реализации) (Листинг 11.15).
spec/models/user_spec.rb
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.
user.followers
использующая реверсированные взаимоотношения. app/models/user.rb
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.2 Веб-интерфейс для читаемых пользователей
Во введении к этой главе, мы сделали предварительный обзор страниц, необходимых для слежения за сообщениями пользователей. В этом разделе мы реализуем базовый интерфейс и читать/не читать функциональность, показанные в тех макетах. Мы также сделаем отдельные страницы для демонстрации массивов читателей и читаемых. В Разделе 11.3, мы завершим наш пример приложения, добавив поток сообщений пользователя.
11.2.1 Образцы данных
Как и в предыдущих главах, нам удобно будет использовать Rake задачу для заполнения базы данных образцами взаимоотношений. Это позволит нам заняться в первую очередь внешним видом страниц, временно отложив серверную часть функциональности.
Когда мы последний раз видели заполнитель образцов данных в Листинге 10.20, он был немного суматошным. Поэтому мы начнем с определения отдельных методов для создания пользователей и микросообщений, а затем добавим образцы данных взаимоотношений используя новый метод make_relationships
. Результаты показаны в Листинге 11.17.
lib/tasks/sample_data.rake
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: "[email protected]",
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.2.2 Статистика и форма для слежения за сообщениями пользователя
Теперь, когда у наших образцов пользователей есть массивы читателей и читаемых, нам нужно обновить главные страницы и страницы профилей, чтобы отразить это. Мы начнем с создания партиала для отображения статистики читаемых и читателей на странице профиля и на главной странице. Затем мы добавим читать/не читать форму, и сделаем отдельные страницы для отображения читаемых и читающих пользователей.
Как было отмечено в Разделе 11.1.1, слово “following” является двусмысленным при использовании в качестве атрибута (где user.following
могло бы означать или читаемых пользователей или читателей пользователя), но оно вполне подходит в качестве метки, как в “50 following”. Кроме того, это метка которой пользуется сам Twitter, использование принятое в набросках начиная с Рис. 11.1 и показанное крупным планом на Рис. 11.10.
Статистика на Рис. 11.10 содержит и количество читаемых пользователей, и количество его читателей, каждое из чисел должно быть ссылкой на соответствующую специальную страницу для их отображения. В Главе 5, мы временно заглушали подобные ссылки знаком ’#’
, но это было до того, как мы набрались опыта с маршрутами. Сейчас, несмотря на то, что мы отложили сами страницы до Раздела 11.2.3, мы сделаем маршруты сейчас, как показано в Листинге 11.18. Этот код использует метод :member
внутри блока resources
, с которым мы ранее не были знакомы, но посмотрим, сможете ли вы угадать, что он делает. (Примечание: код в Листинге 11.18 должен заменить resources :users
.)
following
и followers
в контроллер Users. config/routes.rb
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
.
HTTP request | URL | Действие | Именованный маршрут |
---|---|---|---|
GET | /users/1/following | following | following_user_path(1) |
GET | /users/1/followers | followers | followers_user_path(1) |
Определив маршруты мы готовы сделать тесты партиала статистики. (Мы могли бы начать с тестов, но именованные маршруты было бы трудно объяснить без обновленного файла маршрутов.) Партиал появится на странице профиля и на странице Home;
spec/requests/static_pages_spec.rb
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.
app/views/shared/_stats.html.erb
<% @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
Сравните это с подсчетом количества микросообщений из Листинга 10.17, где мы писали
@user.microposts.count
для подсчета микросообщений.
Одна последняя деталь которую стоит отметить - наличие CSS id у некоторых элементов, как в
<strong id="following" class="stat">
...
</strong>
Это сделано в угоду Ajax реализации из Раздела 11.2.5, которая получает доступ к элементам страницы используя их уникальные id.
С готовым партиалом включить статистику в Home страницу проще простого, как показано в Листинге 11.21. (Это также приводит к прохождению тестов из Листинга 11.19.)
app/views/static_pages/home.html.erb
<% 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.11.
app/assets/stylesheets/custom.css.scss
.
.
.
/* 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;
}
}
.
.
.
Мы подключим статистику к странице профиля через мгновение, но вначале давайте сделааем партиал для follow/unfollow кнопки, как показано в Листинге 11.23.
app/views/users/_follow_form.html.erb
<% 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 (Листинг 10.22), как показано в Листинге 11.24.
config/routes.rb
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.
app/views/users/_follow.html.erb
<%= 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 %>
app/views/users/_unfollow.html.erb
<%= 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.12 и Рис. 11.13.
app/views/users/show.html.erb
<% 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.2.4) и с помощью Ajax (Раздел 11.2.5), но вначале мы закончим HTML интерфейс, создав страницы для списков читающих и читаемых.
11.2.3 Страницы с читаемыми и читателями
Страницы для отображения читающих сообщения пользователя и читаемых им пользователей будут напоминать гибрид страницы профиля пользователя и страницы со списком пользователей (Раздел 9.3.1), с сайдбаром пользовательской информации (включая статистику слежения за сообщениями) и таблицу пользователей. Кроме того, мы включим сетку пользовательских профильных изображений-ссылок в сайдбаре. Набросок соответствующий этим требованиям представлен на Рис. 11.14 (читаемые) и Рис. 11.15 (читатели).
Нашим первым шагом будет получение рабочих ссылок following (читаемые) и followers (читатели). Мы будем следовать примеру Твиттера и обе страницы будут требовать входа пользователя, как было протестировано в Листинге 11.28. Для вошедших пользователей, страницы должны иметь ссылки на читаемых и читателей, соответственно, как протестировано в Листинге 11.29.
spec/requests/authentication_pages_spec.rb
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
followed_users
и followers
страниц. spec/requests/user_pages_spec.rb
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.
following
и followers
. app/controllers/users_controller.rb
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 охватывает их.
show_follow
используемое для рендеринга читаемых и читателей. app/views/users/show_follow.html.erb
<% 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.2.4 Стандартный способ реализации кнопки "читать" (follow)
Теперь, когда наши представления в порядке, пришло время получить рабочие follow/unfollow кнопки. Тесты для этих кнопок комбинируют множество техник тестирования о которых было рассказано в этом учебнике и представляют из себя хорошее упражнение в чтении кода. Изучайте Листинг 11.32 до тех пор пока не убедитесь что вы понимаете что и почему мы тестируем. (Листинг 11.32 содержит одно небольшое упущение в безопасности; посмотрим, сможете ли вы выявить его. Мы скоро расскажем о нем.) Особенно обратите внимание на использование метода have_xpath
использующего продвинутую и мощную технику XPath для навигации по XML документам (включая HTML5). Вы можете узнать больше о XPath с помощью поискового запроса XPath syntax.
spec/requests/user_pages_spec.rb
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
методов вызывающих эти действия напрямую.
spec/requests/authentication_pages_spec.rb
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.
app/controllers/relationships_controller.rb
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/
11.2.5 Реализация кнопки "читать" (follow) с Ajax
Хотя наша реализация слежения за сообщениями пользователей является законченной и в своем нынешнем виде, нам осталось совсем немного подправить ее прежде чем заняться потоком сообщений. Вы могли заметить в Разделе 11.2.4 что оба create
и destroy
действия в контроллере Relationships просто перенаправляют обратно к исходному профилю. Другими словами, пользователь начинает на странице профиля, подписывается на сообщения пользователя и немедленно перенаправляется на исходную страницу. Резонный вопрос - почему пользователь вообще должен покидать эту страницу?
Именно эту проблему решает Ajax, который позволяет веб страницам отправлять асинхронные запросы на сервер не покидая страницы.8 Поскольку практика добавления Ajax в веб формы является довольно распространенной, Rails делает реализацию Ajax легкой. Действительно, обновление партиалов формы follow/unfollow тривиально: просто заменим
form_for
на
form_for ..., remote: true
и Rails автомагически будет использовать Ajax.9 Обновленные партиалы представлены в Листинге 11.35 и Листинге 11.36.
app/views/users/_follow.html.erb
<%= 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 %>
app/views/users/_unfollow.html.erb
<%= 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.)
spec/controllers/relationships_controller_spec.rb
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 пример показывающий более компактный способ выполнить то же самое.)
app/controllers/relationships_controller.rb
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.
app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
$("#followers").html('<%= @user.followers.count %>')
Файл destroy.js.erb
аналогичен (Листинг 11.40).
app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>")
$("#followers").html('<%= @user.followers.count %>')
Теперь вам следует перейти на страницу профиля пользователя и проверить, что вы можете следить и не следить за сообщениями пользователей без обновления страницы, а набор тестов должен позеленеть:
$ bundle exec rspec spec/
Использование Ajax в Rails является большой и стремительно развивающейся темой, и здесь мы лишь слегка коснулись ее, но (как и с остальными материалами в этом учебнике) наш подход дает вам хорошую основу для изучения более продвинутых ресурсов.
11.3 Поток сообщений
Мы подошли к кульминации нашего примера приложения: потоку сообщений. Соответствено, этот раздел содержит некоторые из самых продвинутых материалов в данном учебнике. Полноценный поток сообщений опирается на свой прототип из Раздела 10.3.3 и собирает массив микросообщений из микросообщений читаемых пользователей, совместно с собственными микросообщениями текущего пользователя. Чтобы совершить этот подвиг, нам понадобятся некоторые довольно продвинутые Rails, Ruby и даже SQL техники программирования.
Так как нам предстоит тяжелая работа, особенно важно понимать куда мы будем двигаться. Резюме окончательного потока сообщений пользователя, показанный на Рис. 11.5, вновь показан на Рис. 11.18.
11.3.1 Мотивация и стратегия
Основная идея потока сообщений проста. Рис. 11.19 показывает пример таблицы базы данных microposts
и результирующий поток сообщений. Цель потока заключается в вытягивании микросообщений чей user id соответствует пользователям, сообщения которых читает текущий пользователь (и id самого текущего пользователя), как указано стрелками на схеме.
Поскольку нам необходим способ найти все микросообщения пользователей, за которыми следит данный пользователь, мы запланируем реализацию метода называемого from_users_followed_by
, который мы будем использовать следующим образом:
Micropost.from_users_followed_by(user)
Хотя мы пока не знаем как реализовать его, мы уже можем написать тесты для его функциональности. Ключевым моментом является проверка всех трех требований к потоку: микросообщения читаемых пользователей и самого пользователя должны быт включены в поток, но в него не должны попадать сообщения пользователей, от чтения которых пользователь отписался. Первые два требования уже представлены в наших тестах: Листинг 10.35 проверяет что собственные микросообщения пользователя представлены в потоке, в то время как микросообщений отписанных пользователей быть не должно. Теперь, когда мы знаем как следить за сообщениями пользователей, мы можем добавить третий тип тестов, в этот раз проверяющих что микросообщения читаемых пользователей представлены в потоке, как это показано в Листинге 11.41.
spec/models/user_spec.rb
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.
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
def feed
Micropost.from_users_followed_by(self)
end
.
.
.
end
11.3.2 Первая реализация потока сообщений
Пришло время реализовать 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
, доступного на любом “перечисляемом” объекте, т.е., любом объекте (таком как Массив или Хэш), который состоит из коллекции элементов.10 Мы видели пример этого метода в Разделе 4.3.2; он работает следующим образом:
$ rails console
>> [1, 2, 3, 4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]
Ситуации, подобные той, что показана выше, где такой же метод (например, to_s
) вызывается на каждый элемент, настолько обычная вещь, что есть сокращенная запись, использующая ампресанд &
и символ, соответствующий методу:11
>> [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.
from_users_followed_by
метода. app/models/micropost.rb
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 других пользователей?)
11.3.3 Подзапросы
Как намекалось в последнем разделе, реализация потока сообщений в Section 11.3.2 не очень хорошо масштабируется при большом количестве микросообщений, что скорее всего произойдет если пользователь начнет читать сообщения, скажем, 5000 других пользователей. В этом разделе мы повторно реализуем ленту сообщений способом, который лучше масштабируется с количеством читаемых пользователей.
Проблема с кодом из Раздела 11.3.2 в том что
followed_user_ids = user.followed_user_ids
вытягивает всех читаемых пользователей в память и создает массив длинной во весь список читаемых пользователей. Поскольку условие в Листинге 11.43 на самом деле лишь проверяет включение во множество, должен быть более эффективный способ для этого, да и SQL оптимизирован именно для таких множественных операций. Решение заключается в отправке поиска id читаемых пользователей в базу данных с помощью подзапроса.
Мы начнем с рефакторинга потока немного модифицированным кодом в Листинге 11.44.
from_users_followed_by
. app/models/micropost.rb
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
Этот подзапрос организует всю логику для отправки в базу данных, что является более эффективным.12
С этим фундаментом мы готовы к эффективной релизации потока сообщений, как видно в Листинге 11.45. Обратите внимание, что, так как теперь это чистый SQL, followed_user_ids
является интерполированным, а не маскированным. (На самом деле рабочими являются оба варианта, но мне кажется более логичным интерполировать в данном контексте.)
from_users_followed_by
. app/models/micropost.rb
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.3.4 Новый поток сообщений
С кодом в Листинге 11.45, наш поток сообщений завершен. Напомним, что код для Home страницы, представлен в Листинге 11.46; этот код создает пагинированный поток соответствущих микросообщений для использования в представлении, как видно в Рис. 11.20.13 Отметим, что paginate
метод фактически достигает цели в методе модели Micropost в Листинге 11.45, организуя вытягивание только 30 микросообщений за раз из базы данных. (Вы можете проверить это изучив SQL выражения в логах сервера разработки.)
home
с пагинированным потоком. app/controllers/static_pages_controller.rb
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.4 Заключение
Добавив ленту сообщений, мы закончили ключевой пример приложения Учебника 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
11.4.1 Расширения к примеру приложения
Рекомендуемые расширения в этом разделе в основном вдохновлены основными функциями, общепринятыми для веб-приложений, такими как напоминание пароля и подтверждение адреса электронной почты, или функции характерные для нашего типа примера приложения, такие как поиск, ответы, обмен сообщениями. Реализация одного или более расширений приложения поможет вам сделать переход от выполнения примеров учебника к написанию собственных оригинальных приложений.
Не удивляйтесь, если по началу будет непросто; чистый лист новой фичи может быть немного пугающим. Чтобы помочь вам начать, я могу дать две большие рекомендации. Во-первых, пежде чем добавлять какую-либо функцию к Rails приложению, взгляните на RailsCasts archive чтобы посмотреть, не рассказал ли уже об этом Ryan Bates.14 Если он это сделал, просмотр соответствующего Railscast сэкономит вам массу времени. Во-вторых, всегда делайте обширный поиск в Google по вашей предполагаемой функции, чтобы найти соответствующее сообщения в блогах и пособиях. Разработка веб приложений это непростое дело и это поможет вам учиться на чужом опыте (и ошибках).
Многие из следующих функций являются довольно непростыми задачами, и я дал несколько подсказок по поводу средств, которые могут вам понадобиться для их реализации. Даже с подсказками, они являются намного более трудными чем упражнения, которые приводились в конце каждой главы учебника, так что не расстраивайтесь, если вы не можете решить их без значительных усилий. Из-за нехватки времени я недоступен для личной помощи, но если есть достаточный интерес я мог бы выпустить автономную статью/скринкаст охватывающий эти расширения в будущем; перейдите на основной сайт Rails Tutorial https://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.
11.4.2 Руководство по дальнейшим ресурсам
Существует огромное количество 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 неизменно высокого качества и я горячо рекомендую их.
11.5 Упражнения
- Добавьте тесты для уничтожения взаимоотношений связанных с данным пользователем (т.e., к реализации
dependent :destroy
в Листинге 11.4 и Листинге 11.16). Подсказка: следуйте примеру в Листинге 10.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
- Фотографии для набросков взяты с http://www.flickr.com/photos/john_lustig/2518452221/ и http://www.flickr.com/photos/30775272@N05/2884963755/. ↑
- Первое издание этой книги использовало терминологию
user.following
, в которой даже я иногда путался. Благодарю читателя Cosmo Lee за то что он убедил меня изменить терминологию и за сформулированные советы о том как сделать это более понятным. (Однако не последовал его совету в точности, так что если вы все еще путаетесь - это не его вина.) ↑ - Для простоты, Рис. 11.6 подавляет
id
столбец таблицыfollowing
. ↑ - Более подробно об этом см. when to use let at Stack Overflow. ↑
- Технически, Rails использует
underscore
метод для преобразования имени класса в id. Например,"FooBar".underscore
является"foo_bar"
, поэтому внешним ключом для объектаFooBar
будетfoo_bar_id
. (Кстати, инверсиейunderscore
являетсяcamelize
, который конвертируетcamel_case
вCamelCase
.) ↑ - Если вы заметили что
followed_id
также идентифицирует пользователя, и обеспокоены ассиметричным обращением с читателями и читаемыми, вы готовы к любым неожиданностям. Мы займемся этим вопросом в Разделе 11.1.5. ↑ - Если у вас есть большой опыт моделирования конкретной предметной области, вы зачастую можете предугадать такие вспомогательные методы, и даже если нет, вы часто обнаруживаете себя за их написанием с целью почистить тесты. Однако в данном случае нормально если вы не угадали их. Разработка програмного обеспечения это обычно итеративный процесс — вы пишете код до тех пор пока он не начинает становиться уродливым, а затем вы рефакторите его — но, для краткости, изложение в учебнике немного сглажено. ↑
- Так как номинально это является акронимом asynchronous JavaScript and XML, Ajax иногда ошибочно пишут как “AJAX”, хотя на протяжении всей оригинальной Ajax статьи используется написание “Ajax”. ↑
- Это работает только если JavaScript включен в браузере, но изящно деградирует, работая в точности как в Разделе 11.2.4 если JavaScript отключен. ↑
- Основное требование заключается в том, что перечисляемые объекты должны реализовывать
each
метод для перебора коллекции. ↑ - На самом деле такая нотация на самом деле изначально была расширением которое Rails вносил в ядро языка Ruby; она была настолько полезной, что в настоящее время она включена в сам Ruby. Замечательно, правда? ↑
- Для более продвинутых способов создания необходимых подзапросов, см. сообщение в блоге “Hacking a subselect in ActiveRecord”. ↑
- Для того чтобы сделать поток сообщений на Рис. 11.20 более привлекательным, я добавил несколько дополнительных микросообщений вручную используя Rails консоль. ↑
- Единственная моя оговорка по поводу Railscasts — они обычно опускают тесты. Это, вероятно, необходимо для сохранения красоты и краткости эпизодов, но у вас может сформироваться неправильное представление о важности тестов. После просмотра соответствующего Railscast для получения представления о процессе, я советую писать новую фнкцию используя разработку через тестирование. (В этом контексте я рекомендую взглянуть на the RailsCast on “How I test”. Вы увидите, что Ryan Bates сам обычно использует TDD для разработки в реальной жизни и что, фактически, его стиль тестирования совпадает со стилем используемым в данном учебнике.) ↑
- # позволю себе небольшой комментарий к переводу этой главы. В тексте очень часто употребляются различные формы слова follow (following, follower, followers и т.п.) в дословном переводе это означает "следовать" (слежение, следящий, следящие соответственно). Однако Twitter, (клоном которого является пример приложения, рассматриваемый в этом учебнике) вместо дословного перевода использует термины "Читает" (для following) и "Читают" (для followers) и лично мне термин "Читатели" вместо возможного дословного перевода (следователи, последователи и т.п.) нравится больше. И, несмотря на несколько казусов с этим связанных, в оставшейся части учебника я постараюсь придерживаться именно этого варианта.
P.s. Надеюсь данный комментарий не запутал вас окончательно. :) ↑