среда, 30 января 2013 г.

Запуск системной команды в Ruby

Для запуска команды командной строки через Ruby используется команда

system("commandhere")

Эта команда эквивалентна команде в PHP

exec("commandhere");

В Python аналогичная команда будет записана так

import os
os.system("commandhere")

Пример.

system("compass compile")

Другие примеры.

system 'echo hi' #=> true (prints 'hi')
system 'echo hi >&2' #=> true (prints 'hi' to stderr)
system 'exit 1' #=> nil

среда, 28 ноября 2012 г.

Работа с GitHub

Если вы работаете над проектом в GitHub, то вы можете просто ответвить  проект (fork) в ваш GitHub-репозиторий или создать новый репозиторий под вашим именем. После того, как вы внесете изменения в вашем GitHub-репозитории, вы можете послать запрос на добавление (pull) изменений в GitHub-репозитории других пользователей, уведомив их о внесенных вами изменениях, после чего они смогут добавить (pull) ваши изменения в свои репозитории.

Для примера форкнем проект ClickToFlash. Перейдите в проект по адресу http://github.com/rentzsch/clicktoflash и кликните на кнопку "Fork". После этого копия данного проекта появится в вашем GitHub-репозиторий (например http://github.com/lapcat/clicktoflash). Теперь вам необходимо клонировать код из вашего репозитория на ваш локальный компьютер. Убедитесь в том, что вы клонируете SSH-версию. Если вы клонируете read-only версию, тогда вы не сможете добавлять (pull) изменения в ваш GitHub-репозиторий.

$ git clone git@github.com:lapcat/clicktoflash.git


Ваша частная локальная копия автоматически будет иметь master branch ветку, которая соответствует master branch ветке в ваше публичном репозитории на GitHub.

$ cd clicktoflash
$ git branch
* master
$ git status
# On branch master nothing to commit (working directory clean)

Ваш удаленный клонированный репозиторий дал особое имя origin вашему локальному клону репозитория.

$ git remote
origin

Локальная ветка master branch всегда отслеживает удаленный репозиторий на GitHub, поэтому команды git fetch, git pull и git push автоматически добавляются  в origin, когда запускается checked out в master ветке.

$ git remote show origin
* remote origin
  Fetch URL: git@github.com:lapcat/clicktoflash.git
  Push  URL: git@github.com:lapcat/clicktoflash.git
  HEAD branch: master
  Remote branches:
    cutting-edge tracked
    master       tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

Несмотря на тот факт, что локальный репозиторий является клоном origin, а origin - это форк (fork) из репозитория пользователя rentzsch, локальный репозиторий ничего не знает о rentzsch. Поэтому, если вы хотите добавить (pull) ваши изменения и репозитория rentzsch, то вы должны добавить (add) удаленный репозиторий. В этом случае вы можете использовать read-only URL, из-за того, что вы не можете передать изменения в репозиторий rentzsch напрямую.

$ git remote add rentzsch git://github.com/rentzsch/clicktoflash.git
$ git remote
origin
rentzsch

Некоторые люди предпочитают давать имя удаленному репозиторию upstream, но я нахожу это несколько запутывающим. Имя rentzsch говорит точно откуда приходят изменения. В отличии от имени upstream оно не абстрактно и его не спутаешь с origin.

Обратите внимание, что пока вы не используете опцию -f, удаленный репозиторий rentzsch автоматически не фетчится. Поэтому вам нужно фетчить (получать из него данные) вручную.

$ git branch rentzsch-master rentzsch/master
fatal: Not a valid object name: 'rentzsch/master'.
$ git branch -r
  origin/HEAD -> origin/master
  origin/cutting-edge
  origin/master
$ git fetch rentzsch
$ git branch -r
  origin/HEAD -> origin/master
  origin/cutting-edge
  origin/master
  rentzsch/1.4.2-64bit
  rentzsch/cutting-edge
  rentzsch/gh-pages
  rentzsch/master
$ git branch rentzsch-master rentzsch/master
Branch rentzsch-master set up to track remote branch master from rentzsch.

Я рекомендую создать вам ответвление branch для отслеживания форкнутого репозитория, как мы это сделали в последнуй инструкции выше. После этого будет не важно какие изменения вы сделаете, вы всегда сможете посмотреть "официальную" версию проекта, делая check rentzsch-master branch. Если вы используете удаленный rentzsch-master branch как стартовую точку для локального ответвления rentzsch-master, то локальный branch автоматически будет отслеживать удаленный репозиторий rentzsch, как если бы локальный master автоматически отслеживал удаленный origin.

$ git remote show rentzsch
* remote rentzsch
  Fetch URL: git://github.com/rentzsch/clicktoflash.git
  Push  URL: git://github.com/rentzsch/clicktoflash.git
  HEAD branch: master
  Remote branches:
    1.4.2-64bit  tracked
    cutting-edge tracked
    gh-pages     tracked
    master       tracked
  Local branch configured for 'git pull':
    rentzsch-master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (fast-forwardable)

Когда изменения происходят в master branch удаленного репозитория rentzsch, то мы используем процедуру merge для слияния их:

$ git checkout rentzsch-master
$ git fetch
$ git merge rentzsch/master
$ git checkout master
$ git merge rentzsch-master
$ git push

Вы можете использовать одну команду git pull вместо двух git fetch и git merge rentzsch/master. Однако я слышал, что git pull иногда вызывает проблемы.

В любом случае, вы сперва делаете слияние (merge) удаленного репозитория rentzsch с локальной веткой rentzsch-master branch. Затем делаете слияние (merge) ветки rentzsch-master branch с локальной master branch и, наконец, добавляете (pull) локальные изменения в удаленный репозиторий origin. Эта процедура необходима, потому что вы не можете напрямую добавить изменения из удаленного репозитория rentzsch в origin. Они должны пройти через локальный репозиторий.

Никогда не записывайте код в локальный master branch, иначе все сломается. Оставляйте master чистым.

Когда вы хотите сделать локальные изменения, то всегда создавайте и делайте check out в новый branch, а затем делайте слияние (merge) изменений branch, в который вы хотите добавить изменения.

$ git branch rentzsch-cutting-edge rentzsch/cutting-edge
Branch rentzsch-cutting-edge set up to track remote branch cutting-edge from rentzsch.
$ git branch cutting-edge rentzsch-cutting-edge
$ git checkout cutting-edge
Switched to branch 'cutting-edge'

пятница, 2 ноября 2012 г.

Ruby on Rails MySQL

Если вы используете Ruby on Rails 3, то для создания приложения с установленной базой данных MySQL вместо sqlite3 наберите в консоли команду:

rails new your_project_name -d mysql

Если вы используете более раннюю версию Ruby on Rails, то наберите в консоли команду:

rails new -d mysql your_project_name

Для того, чтобы узнать номер вашей версии Ruby on Rails наберите в консоли:

rails -v

среда, 31 октября 2012 г.

Ruby on Rails Подсказки

Запомнить, где именно в модели в Ruby on Rails нужно помещать инструкцию belongs_to, несложно: если у таблицы есть внешние ключи (англ. foreign key), то соответствующая модель должна иметь для каждого из них инструкцию belongs_to.

Здесь проиллюстрировано одно важное правило: модель для таблицы, имеющей
внешний ключ (англ. foreign key), всегда содержит объявление belongs_to.

В Active Record родительский объект (тот самый, который логически содержит коллекцию дочерних объектов) использует для объявления своих связей с дочерней таблицей has_many, а для дочерней таблицы, чтобы указать на ее родителя, используется объявление belongs_to.

Связи многие ко многим являются симметричными, обе связываемые таблицы объявляют свою связь друг с другом, используя has_and_belongs_to_many.

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




product_path против product_url

Поначалу кажется, что будет довольно сложно узнать, когда при необходимости создать ссылку или перенаправление по заданному маршруту нужно использовать метод product_path, а когда метод product_url. Но на самом деле все довольно просто.

При использовании метода product_url вы получите полную начинку с протоколом и доменным именем, наподобие http://example.com /products/l. Его следует использовать, если осуществляется перенаправление redirect_to, поскольку спецификация HTTP при осуществлении перенаправлений с кодом 302 и им подобных требует указывать URL-адрес полностью.

Полный URL-адрес нужен также при перенаправлении с одного домена на другой, например, product_url(domain: "example2.com", product: product).
Во всех остальных случаях можно с успехом использовать product_path. Этот метод будет енерировать только часть пути /products/1, а для ссылок или указания форм вроде link_to "My lovely product", produc_path (product) больше ничего и не нужно.

Путаница возникает из-за того, что снисходительность браузеров зачастую делает эти два метода взаимозаменяемыми. Перенаправление redirect_to можно производить с product_path, и такой вариант, скорее всего, сработает, но с точки зрения спецификации он будет считаться неправильным. Точно так ж е можно при ссылке link_to использовать product_url, но тогда ваш HTML будет засорен ненужными символами, что также нельзя признать удачным вариантом.

вторник, 30 октября 2012 г.

Установка Heroku и развертывание приложения на сервере

На Heroku желательно устанавливать приложения, использующие базу данных Postgress, но подойдет и MySQL. База данных sqlite3 работать не будет.

После создания учетной записи Heroku, установите гем Heroku, набрав в консоли:

gem install heroku

Как и с GitHub, при использовании Heroku вы должны будете создать ключи SSH если у вас их еще нет, а затем сообщить Heroku свой открытый ключ так, чтобы можно было использовать Git, для отправки репозитория примера приложения на их серверы:

 heroku keys:add

Наконец, используйте heroku команду, чтобы создать для примера приложения место на серверах Heroku:

heroku create

Да, вот и все. Нeroku команда создает новый субдомен только для нашего приложения, доступный для немедленного просмотра. Там еще ничего нет, так что давайте займемся развертыванием.

Первый шаг для развертывания на Heroku это отправка приложения на Heroku с использованием Git:

git push heroku master

Чтобы увидеть ваше свежеразвернутое приложение, нужно посетить адрес, который вы видели, когда выполняли, heroku create. Вы также можете использовать аргумент heroku команды который автоматически откроет ваш браузер с правильным адресом:

heroku open

Есть тонны команд Heroku. Уделю минуту, чтобы показать только одну из них, переименовав приложение следующим образом:

heroku rename railstutorial

Установка Git на Windows и его использование совместно с GitHub

Установка Git в Windows очень проста.
У проекта msysGit процедура установки - одна из самых простых. Просто скачайте файл exe-инсталлятора со страницы Google Code и запустите его:

code.google.com/p/msysgit

После установки у вас будет как консольная версия (включающая SSH-клиент, который пригодится позднее), так и стандартная графическая.

После установки Git следует выполнить ряд разовых настроек. Это системные настройки, что означает, что их необходимо сделать лишь единожды.
Наберите в консоли:

git config --global user.name "Your Name"
git config --global user.email youremail@example.com

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

git init

Следующий шаг добавит файлы проекта в репозиторий. Есть незначительная сложность: Git по умолчанию отслеживает изменения всех файлов, но есть несколько файлов, которые мы не хотели бы отслеживать. Например, Rails создает log файлы, для записи поведения приложения; эти файлы часто изменяются, и мы не хотим, чтобы наша система управления версиями постоянно обновляла их. у Git есть простой механизм, чтобы игнорировать такие файлы: просто включите файл, названный .gitignore в корневой каталог Rails с небольшим количеством правил, которые говорят Git какие файлы следует игнорировать.

Дефолтный .gitignore создаваемый командой rails.

.bundle
db/*.sqlite3
log/*.log
tmp/**/*

Листинг заставляет Git игнорировать такие файлы как log файлы, Rails временные (tmp) файлы, и базы данных SQLite. (Например, чтобы игнорировать log файлы, которые живут в log/ каталоге, мы используем log/*.log, чтобы игнорировать все файлы, заканчивающиеся на .log.) Большинство этих игнорируемых файлов изменяются часто и автоматически, так что включать их в управление версиями неудобно; кроме того, при совместной разработке они могут вызвать конфликты.

Наконец, мы добавим файлы вашего проекта к Git, а затем закоммитим результаты. Можно добавить все файлы (кроме тех, которые соответствуют правилам игнорирования в .gitignore) следующим образом, набрав в консоли:

git add .

Здесь точка ‘.’ представляет текущий каталог, и Git достаточно умен, чтобы добавить файлы рекурсивно, таким образом, это автоматически включает все подкаталоги. Эта команда добавляет файлы проекта в зону ожидания, которая содержит незавершенные изменения вашего проекта; можно видеть, какие файлы находятся в зоне ожидания, используя команду status:

git status

Для того, чтобы сказать Git, что вы хотите сохранить изменения, используйте команду commit:

git commit -m "Moy perviy commit"

Метка -m позволяет вам добавлять сообщение для фиксации.

Важно отметить, что коммиты Git локальны, и записываются только на машине, на которой происходят коммиты. Что отличает его от популярной open-source системы управления версиями под названием Subversion, в которой коммит обязательно приводит к изменениям в удаленном репозитарии. Git делит коммит в стиле Subversion на два логических куска: локальная запись изменений (git commit) и отправка изменений в удаленный репозиторий (git push)..

Между прочим, вы можете увидеть список своих сообщений о коммитах, используя команду log:

git log

Чтобы выйти из log git, нажмите q.

Мы можем легко отменить изменения в файлах, получив из Git предыдущую фиксацию командой checkout (и флагом -f, чтобы инициировать перезапись текущих изменений):

git checkout -f

Теперь, когда вы поместили свой проект в систему управления версиями Git, пора отправить ваш код на GitHub - веб-сервис для хостинга исходного кода проектов и их совместной разработки основанный на системе контроля версий Git.

После регистрации на GitHub (github.com) вы увидите следующую страницу:

github_first_page

Щелкните создать репозиторий и заполните форму.

create_first_repository

После подтверждения формы отправьте свое первое приложение, набрав в консоли следующие команды:

git remote add origin git@github.com:<username>/your_app.git
git push origin master

Эти команды говорят Git, что вы хотите добавить GitHub как начальный адрес для вашей основной (master) ветки, а затем отправить ваш репозиторий на GitHub. Конечно, следует заменить <username> вашим фактическим именем пользователя. Например, команда, которую я запустил для railstutorial пользователя, была

git remote add origin git@github.com:railstutorial/first_app.git


Результатом является страница на GitHub для репозитория первого приложения (first application), с браузером файлов, полной историей коммитов.



github_repository_page

Git невероятно хорош в создании веток, которые фактически являются копиями репозитория, где мы можем произвести (возможно экспериментальные) изменения, не модифицируя родительские файлы. В большинстве случаев родительский репозиторий – master ветка, и мы можем создать новую рабочую ветку используя checkout с флагом -b:

git checkout -b izmenenie-README
git branch

Здесь вторая команда, git branch, только перечисляет все локальные ветки, а звездочка * указывает, какая ветка в настоящий момент включена.

Отметьте, что git checkout -b izmenenie-README одновременно и создает новую ветку и переключает на нее, на что указывает звездочка перед izmenenie-README веткой.

После внесения изменений в файлы мы могли бы использовать git add . , но Git предусматривает флаг -a как сокращение для (очень частого) случая фиксации всех изменений к существующим файлам (или файлов, созданных с использованием git mv, которые не считаются новыми для Git):

git commit -a -m "Uluchshenie fayla README"

Будьте осторожны с использованием флага -a; если вы добавили какие-либо новые файлы в проект после последней фиксации, вы должны сообщить Git о них используя сначала git add.Будьте осторожны с использованием флага -a; если вы добавили какие-либо новые файлы в проект после последней фиксации, вы должны сообщить Git о них используя сначала git add.

Теперь, когда мы закончили производить наши изменения, мы готовы объединить результаты с нашей master веткой:

git checkout master
git merge izmenenie-README

После того, как вы объединили изменения, вы можете почистить свои ветки, стерев тему ветки, используя git branch -d, если вы хотите, сделайте это так:

git branch -d izmenenie-README

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

Как упомянуто выше, вы также можете отказаться от изменений, относящихся к рабочей ветке, в таком случае используйте git branch -D:

# Только для иллюстрации; не делайте этого, если планируете в дальнейшем пользоваться веткой

git checkout -b topic-branch
git add .
git commit -a -m "Screwed up"
git checkout master
git branch -D topic-branch

В отличие от флага -d, флаг -D сотрет ветку даже если мы не объединили изменения.

Теперь, когда мы обновили README, мы можем отправить изменения в GitHub чтобы увидеть результат. Так как мы уже сделали одну отправку, на большинстве систем, мы теперь можем опустить origin master, и просто выполнить git push:

git push

На некоторых системах, эта команда выдает ошибку:

git push
fatal: The current branch master is not tracking anything.

В этом случае необходимо выполнить команду:

git push origin master

пятница, 12 октября 2012 г.

Ruby on Rails Tutorial 12

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

В этой главе мы завершим ядро примера приложения, добавив социальный слой, что позволит пользователям читать (и не читать) сообщения других пользователей 27, в результате чего на главной странице каждого пользователя будет отображаться список микросообщений тех пользователей, сообщения которых он читает. Мы также сделаем представления для отображения читающих и читаемых пользователей. Мы узнаем, как смоделировать слежение за сообщениями пользователей в Разделе 12.1, а затем сделаем веб-интерфейс в Разделе 12.2 (включая введение в Ajax). Наконец, мы закончим, разработав полнофункциональную ленту сообщений в Разделе 12.3.
Эта последняя глава содержит несколько из наиболее сложных материалов учебника, в том числе, сложные модели данных и несколько Ruby / SQL хитростей для создания потока сообщений. С помощью этих примеров вы увидите как Rails может обрабатывать даже весьма сложные модели данных, что должно вам пригодиться, так как вы двигаетесь к разработке собственных приложений с их собственными требованиями. Чтобы помочь с переходом от учебника к самостоятельной разработке, Раздел 12.4 содержит рекомендуемые расширения к ядру примера приложения, а также ссылки на более продвинутые ресурсы.
Как обычно, Git пользователи должны создать новую тему ветки:
$ git checkout -b following-users
Так как материал этой главы особенно сложен, прежде чем писать код, мы улучим момент и сделаем небольшой обзор функции слежения за сообщениями пользователей. Как и в предыдущих главах, на этом раннем этапе мы будем представлять страницы используя макеты.1 Полная последовательность страниц работает следующим образом пользователь, (John Calvin) начинает на странице своего профиля (Рис. 12.1) и переходит на страницу с пользователями (Рис. 12.2) чтобы выбрать пользователя, сообщения которого он будет читать. Calvin переходит на страницу профиля выбранного пользователя, Thomas-а Hobbes-а (Рис. 12.3), кликает по кнопке “Follow”, чтобы читать сообщения этого пользователя. Это изменяет кнопку “Follow” на “Unfollow”, и увеличивает количество “followers” товарища Hobbes-а на единицу (Рис. 12.4). Вернувшись на свою главную страницу, Calvin теперь видит увеличившееся количество “following” и обнаруживает микросообщения Hobbes-а в своей ленте сообщений (Рис. 12.5). Остальная часть этой главы посвящена реализации этой последовательности.
page_flow_profile_mockup
Рисунок 12.1: Макет профиля текущего пользователя. (полный размер)
page_flow_user_index_mockup
Рисунок 12.2: Макет поиска пользователя для чтения его сообщений. (полный размер)
page_flow_other_profile_follow_button_mockup
Рисунок 12.3: Макет профиля другого пользователя с “Follow” кнопкой. (полный размер)
page_flow_other_profile_unfollow_button_mockup
Рисунок 12.4: Макет профиля с “Unfollow” кнопкой и увеличившимся количеством читателей. (полный размер)
page_flow_home_page_feed_mockup
Рисунок 12.5: Макет главной страницы пользователя с лентой сообщений и увеличившимся количеством читаемых пользователей. (полный размер)

12.1 Модель Relationship

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

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

В качестве первого шага на пути построения модели данных для слежения за сообщениями пользователей, давайте рассмотрим следующий типичный случай. Возьмем, в качестве примера, пользователя, который следит за сообщениями второго пользователя: мы могли бы сказать, что, например, Кальвин читает сообщения Гоббса, и Гоббс читается Кальвином, таким образом, Кальвин является читателем, а Гоббс является читаемым. При использовании дефолтной Rails’ плюрализации, множество таких читаемых пользователей называлось бы followeds, но это безграмотно и неуклюже; вместо этого мы перепишем умолчание и назовем их читаемые, и user.following будет содержать массив пользователей, за сообщениями которых следит текущий пользователь. Аналогично, множество пользователей, читающих данного пользователя это читатели пользователя, и user.followers будет массивом таких пользователей.
Это предполагает моделирование читаемых пользователей как на Рис. 12.6, с following таблицей и has_many ассоциацией. Поскольку user.following должно быть массивом пользователей, каждая строка таблицы following должна быть пользователем, идентифицируемым с помощью followed_id, совместно с follower_id для установления ассоциации.2 Кроме того, так как каждая строка является пользователем, мы должны были бы включить другие атрибуты пользователя, включая имя, пароль и т.д.
naive_user_has_many_following
Рисунок 12.6: Наивная реализация слежения за сообщениями пользователя.
Проблема модели данных из Рис. 12.6 в том, что она ужасно избыточна: каждая строка содержит не только id каждого читаемого пользователя, но и всю остальную информацию, уже содержащуюся в таблице users. Еще хуже то, что для моделирования читателей пользователя нам потребуется отдельная followers таблица. Наконец, эта модель данных кошмарно неудобна в эксплуатации, так как каждый раз, при изменении пользователем (скажем) своего имени, нам пришлось бы обновлять запись пользователя не только в users таблице, но также каждую строку, содержащую этого пользователя в обоих following и followers таблицах.
Проблема здесь в том, что нам не хватает лежащей в основе абстракции (an underlying abstraction (??)). Один из способов найти правильную абстракцию, это рассмотреть, как мы могли бы реализовать following в веб-приложении. Вспомним из Раздела 6.3.3, что REST архитектура включает в себя ресурсы которые создаются и уничтожаются. Это приводит нас к двум вопросам: Что создается, когда пользователь начинает читать сообщения другого пользователя? Что уничтожается, когда пользователь прекращает следить за сообщениями другого пользователя?
Поразмыслив, мы видим, что в этих случаях приложение должно создать либо разрушить взаимоотношение (или связь3) между двумя пользователями. Затем пользователь has_many :relationships (имеет_много :взаимоотношений), и имеет много following (или followers) через эти взаимоотношения. Действительно, Рис. 12.6 уже содержит бОльшую часть реализации: поскольку каждый читаемый пользователь уникально идентифицирован посредством followed_id, мы можем преобразовать following в таблицу relationships, опустив информацию о пользователе, и использовать followed_id для получения читаемых пользователей из users таблицы. Кроме того, приняв во внимание обратные взаимоотношения, мы могли бы использовать follower_id столбец для извлечения массива читателей пользователя.
Чтобы создать following массив пользователей, мы могли бы вытянуть массив атрибутов followed_id, а затем найти пользователя для каждого из них. Однако, как и следовало ожидать, в Rails есть более удобный способ для этой процедуры; соответствующая техника известна как has_many :through (имеет_много :через).4 Как мы увидим в Разделе 12.1.4, Rails позволяет нам сказать, что пользователь следит за сообщениями многих пользователей через взаимоотношения, используя краткий код
has_many :following, :through => :relationships, :source => "followed_id"
Этот код автоматически заполняет user.following массивом читаемых пользователей. Схема модели данных представлена на Рис. 12.7.
user_has_many_following
Рисунок 12.7: Модель слежения пользователя за сообщениями через промежуточную модель (Взаимоотношений) Relationship. (полный размер)
Чтобы начать работу над реализацией, мы сначала генерируем модель Relationship следующим образом:
$ rails generate model Relationship follower_id:integer followed_id:integer
Так как мы будем искать взаимоотношения по follower_id и по followed_id, мы должны добавить индекс на каждой колонке для повышения эффективности поиска, как показано в Листинге 12.1.
Листинг 12.1. Добавление индексов для relationships таблицы.
db/migrate/<timestamp>_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration
  def self.up
    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

  def self.down
    drop_table :relationships
  end
end
Листинг 12.1 включает также составной индекс, который обеспечивает уникальность пар (follower_id, followed_id), так что пользователь не может следить за сообщениями другого пользователя более одного раза:
add_index :relationships, [:follower_id, :followed_id], :unique => true
(Сравните с email индексом уникальности из Листинга 6.22.) Как мы увидим в Разделе 12.1.4, наш пользовательский интерфейс не позволит этому случиться, но добавление индекса уникальности позволит избежать ошибки в случае, если пользователь попытается дублировать взаимотношения любым другим способом (используя, например, инструмент командной строки, такой как cURL). Мы могли бы также добавить валидацию уникальности к модели Relationship, но так как дублирование взаимоотношений является ошибкой всегда, для наших целей вполне достаточно индекса уникальности.
Для создания таблицы relationships, мы мигрируем базу данных и подготавливаем тестовую бд, как обычно:
$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare
Результирующая модель данных Relationship показана на Рис. 12.8.
relationship_model
Рисунок 12.8: Модель данных Relationship.
Как и с любой новой моделью, прежде чем двигаться дальше, мы должны определить доступные атрибуты модели. В случае с моделью Relationship, followed_id должен быть доступным, поскольку пользователи будут создавать взаимоотношения через веб, но атрибут follower_id должен быть недоступным; в противном случае злоумышленник может вынудить других пользователей следить за его сообщениями. Результат представлен в Листинге 12.2.
Листинг 12.2. Открытие доступа к followed_id атрибуту модели Relationship (но не к follower_id).
app/models/relationship.rb
class Relationship < ActiveRecord::Base
  attr_accessible :followed_id
end

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

Прежде чем приступить к реализации читателей и читаемых, нам вначале необходимо установить ассоциацию между пользователями и взаимоотношениями. Пользователь has_many (имеет_много) взаимоотношений, и, так как взаимоотношения включают двух пользователей — взаимоотношение belongs_to (принадлежит_к) читающим и читаемым пользователям.
Как и с микросообщениями в Разделе 11.1.2, мы будем создавать новые взаимоотношения используя ассоциацию, с помощью такого кода
user.relationships.create(:followed_id => ...)
Мы начнем с теста, показанного в Листинге 12.3, который устанавливает переменную экземпляра @relationships (используется ниже) и проверяет, что она может быть сохранена используя save!. Как и create!, метод save! вызывает исключение в случае неудачного сохранения; сравните это с использованием create! в Листинге 11.4.
Листинг 12.3. Тестирование создания Relationship с save!.
spec/models/relationship_spec.rb
require 'spec_helper'

describe Relationship do

  before(:each) do
    @follower = Factory(:user)
    @followed = Factory(:user, :email => Factory.next(:email))

    @relationship = @follower.relationships.build(:followed_id => @followed.id)
  end

  it "should create a new instance given valid attributes" do
    @relationship.save!
  end
end
Мы также должны протестировать модель User на relationships атрибут, как показано в Листинге 12.4.
Листинг 12.4. Тестирование на user.relationships атрибут.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "relationships" do

    before(:each) do
      @user = User.create!(@attr)
      @followed = Factory(:user)
    end

    it "should have a relationships method" do
      @user.should respond_to(:relationships)
    end
  end
end
На этом этапе вы могли бы ожидать код приложения, как в Разделе 11.1.2, и он аналогичен, но есть одно существенное отличие: в случае с моделью Micropost мы могли бы сказать
class Micropost < ActiveRecord::Base
  belongs_to :user
  .
  .
  .
end
и
class User < ActiveRecord::Base
  has_many :microposts
  .
  .
  .
end
так как в microposts есть атрибут user_id для идентификации пользователя (Раздел 11.1.1). Id используемый таким способом для связи двух таблиц базы данных, известен как внешний ключ, и когда внешним ключом для объекта модели User является user_id, Rails может вывести ассоциацию автоматически: по умолчанию, Rails ожидает внешний ключ в форме <class>_id, где <class> является строчной версией имени класса.5 В данном случае, несмотря на то, что мы по прежнему имеем дело с пользователями, они теперь отождествляются с внешним ключом follower_id, поэтому мы должны сообщить об этом Rails, как показано в Листинге 12.5.6
Листинг 12.5. Реализация 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 к ассоциации; написание теста на это останется в качестве упражнения (Раздел 12.5).) На данный момент, тест ассоциации из Листинга 12.3 и Листинга 12.4 должен пройти.
Как и у модели Micropost, у Relationship модели есть belongs_to взаимоотношения с пользователями; в данном случае, объект взаимоотношение принадлежит к обоим follower и followed пользователям, что мы и тестируем в Листинге 12.6.
Листинг 12.6. Тестирование belongs_to ассоциации пользователь/взаимоотношения.
spec/models/relationship_spec.rb
describe Relationship do
  .
  .
  .
  describe "follow methods" do

    before(:each) do
      @relationship.save
    end

    it "should have a follower attribute" do
      @relationship.should respond_to(:follower)
    end

    it "should have the right follower" do
      @relationship.follower.should == @follower
    end

    it "should have a followed attribute" do
      @relationship.should respond_to(:followed)
    end

    it "should have the right followed user" do
      @relationship.followed.should == @followed
    end
  end
end
Чтобы написать код приложения, мы определяем belongs_to взаимоотношения как обычно. Rails выводит названия внешних ключей из соответствующих символов (т.e., follower_id из :follower, и followed_id из :followed), но, так как нет ни Followed ни Follower моделей, мы должны снабдить их именем класса User. Результат показан в Листинге 12.7.
Листинг 12.7. Добавление belongs_to ассоциаций к модели Relationship.
app/models/relationship.rb
class Relationship < ActiveRecord::Base
  attr_accessible :followed_id

  belongs_to :follower, :class_name => "User"
  belongs_to :followed, :class_name => "User"
end
Ассоциация followed на самом деле не потребуется до Раздела 12.1.5, но параллельность структуры читатели/читаемые лучше видна при одновременной реализации.

12.1.3 Валидации

Прежде чем двигаться дальше, мы добавим пару валидаций модели Relationship для комплектности. Тесты (Листинг 12.8) и код приложения (Листинг 12.9) просты.
Листинг 12.8. Тестирование валидаций модели Relationship.
spec/models/relationship_spec.rb
describe Relationship do
  .
  .
  .
  describe "validations" do

    it "should require a follower_id" do
      @relationship.follower_id = nil
      @relationship.should_not be_valid
    end

    it "should require a followed_id" do
      @relationship.followed_id = nil
      @relationship.should_not be_valid
    end
  end
end
Листинг 12.9. Добавление валидаций модели Relationship.
app/models/relationship.rb
class Relationship < ActiveRecord::Base
  attr_accessible :followed_id

  belongs_to :follower, :class_name => "User"
  belongs_to :followed, :class_name => "User"

  validates :follower_id, :presence => true
  validates :followed_id, :presence => true
end

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

Теперь мы переходим к сердцу ассоциаций Relationship: following и followers. Мы начнем с following, как показано в Листинге 12.10.
Листинг 12.10. Тест для атрибута user.following.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "relationships" do

    before(:each) do
      @user = User.create!(@attr)
      @followed = Factory(:user)
    end

    it "should have a relationships method" do
      @user.should respond_to(:relationships)
    end

    it "should have a following method" do
      @user.should respond_to(:following)
    end
  end
end
Реализация впервые использует has_many :through: пользователь имеет много читаемых (пользователей) через взаимоотношения, как показано на Рис. 12.7. По умолчанию, в ассоциации has_many :through Rails ищет внешний ключ, соответствующий ассоциации в единственном числе; другими словами, код
has_many :followeds, :through => :relationships
будет составлять массив, используя followed_id в таблице relationships. Но, как отмечалось в Разделе 12.1.1, user.followeds это довольно неуклюже; гораздо более естественным будет использование “following” в качестве множественного числа для “followed”, и написание user.following для массива читаемых пользователей. Естественно, Rails позволяет переопределить умолчание, в данном случае с помощью :source параметра (Листинг 12.11), что явно говорит Rails, что источником массива following является множество followed id.
Листинг 12.11. Добавление к модели User ассоциации following с has_many :through.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  has_many :microposts, :dependent => :destroy
  has_many :relationships, :foreign_key => "follower_id",
                           :dependent => :destroy
  has_many :following, :through => :relationships, :source => :followed
  .
  .
  .
end
Чтобы создать взаимоотношение с читаемыми (пользователями), мы введем служебный метод follow! и мы сможем написать user.follow!(other_user).7 Мы также добавим связанный булев метод following? для того чтобы протестировать, читает ли пользователь сообщения других пользователей.8 Тесты в Листинге 12.12 показывают как мы планируем использовать эти методы на практике.
Листинг 12.12. Тесты для некоторых служебных методов “following”.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "relationships" do
    .
    .
    .
    it "should have a following? method" do
      @user.should respond_to(:following?)
    end

    it "should have a follow! method" do
      @user.should respond_to(:follow!)
    end

    it "should follow another user" do
      @user.follow!(@followed)
      @user.should be_following(@followed)
    end

    it "should include the followed user in the following array" do
      @user.follow!(@followed)
      @user.following.should include(@followed)
    end
  end
end
Обратите внимание, что мы заменили метод include? виденный в Листинге 11.31 на should include, преобразовав
@user.following.include?(@followed).should be_true
в более ясный и краткий
@user.following.should include(@followed)
Этот пример показывает, насколько гибкой является булевая конвенция RSpec; несмотря на то, что include уже является ключевым словом Ruby (используется для включения модуля, как мы видели, например, в Листинге 9.11), в этом контексте RSpec правильно угадывает, что мы хотим протестировать включение массива.
В коде приложения, метод following? принимает пользователя, называемого followed, и проверяет, существует ли он в базе данных; метод follow! вызывает create! через relationships ассоциацию для создания взаимоотношения с читаемым. Результаты представлены в Листинге 12.13.9
Листинг 12.13. Служебные методы following? и follow!.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def self.authenticate_with_salt(id, stored_salt)
    .
    .
    .
  end

  def following?(followed)
    relationships.find_by_followed_id(followed)
  end

  def follow!(followed)
    relationships.create!(:followed_id => followed.id)
  end
  .
  .
  .
end
Отметим, что в Листинге 12.13 мы опустили self пользователя, написав просто
relationships.create!(...)
вместо эквивалентного кода
self.relationships.create!(...)
Явное включение или невключение self в данном случае дело вкуса.
Конечно, пользователи должны иметь возможность прекратить слежение за сообщениями других пользователей, что приводит нас к немного предсказуемому методу unfollow!, как показано в Листинге 12.14.10
Листинг 12.14. Тест для прекращения слежения за сообщениями пользователя.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "relationships" do
    .
    .
    .
    it "should have an unfollow! method" do
      @followed.should respond_to(:unfollow!)
    end

    it "should unfollow a user" do
      @user.follow!(@followed)
      @user.unfollow!(@followed)
      @user.should_not be_following(@followed)
    end
  end
end
Код для unfollow! прост: нужно просто найти взаимоотношение по followed id и уничтожить его (Листинг 12.15).11
Листинг 12.15. Прекращение слежения за сообщениями пользователя посредством уничтожения взаимоотношения.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def following?(followed)
    relationships.find_by_followed_id(followed)
  end

  def follow!(followed)
    relationships.create!(:followed_id => followed.id)
  end

  def unfollow!(followed)
    relationships.find_by_followed_id(followed).destroy
  end
  .
  .
  .
end

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

Последняя часть головоломки отношений это добавление метода user.followers, сопутствующего user.following. Вы могли заметить в Рис. 12.7 что все сведения, необходимые для извлечения массива читателей уже присутствуют в таблице relationships. Действительно, техника та же, что и для читаемых пользователей, но с реверсированием ролей follower_id и followed_id. Это говорит о том, что, если бы мы смогли как-то организовать таблицу reverse_relationships, поменяв местами эти два столбца (Рис. 12.9), то мы бы с легкостью реализовали user.followers.
user_has_many_followers
Рисунок 12.9: Модель данных для читающих пользователей, использующая реверсированную модель Relationship. (полный размер)
Начнем с тестов, веря, что магия Rails выручит нас (когда дело дойдет до реализации) (Листинг 12.16).
Листинг 12.16. Тестирование перевернутых взаимоотношений.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "relationships" do
    .
    .
    .
    it "should have a reverse_relationships method" do
      @user.should respond_to(:reverse_relationships)
    end

    it "should have a followers method" do
      @user.should respond_to(:followers)
    end

    it "should include the follower in the followers array" do
      @user.follow!(@followed)
      @followed.followers.should include(@user)
    end
  end
end
Как вы наверное подозреваете, мы не будем создавать полную таблицу в бд для того чтобы просто повести реверс взаимоотношений. Вместо этого мы воспользуемся основной симметрией между читаемыми и читателями для симуляции таблицы 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 затем строится через реверсированные взаимоотношения, как показано в Листинге 12.17.
Листинг 12.17. Реализация 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
(Как и с Листингом 12.5, тест для dependent :destroy остается в качестве упражнения (Раздел 12.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 :following, но вы можете пропустить его.
С кодом в Листинге 12.17, ассоциации читаемые/читатели завершены, и все тесты должны пройти. Этот раздел предъявил довольно высокие требования к вашим навыкам моделирования данных, и это нормально, если для его усвоения потребуется некоторое время. Фактически, одним из самых лучших способов понять ассоциации является их использование в веб интерфейсе, как мы увидим в следующем разделе.

12.2 Веб-интерфейс для читаемых и читателей

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

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

Как и в предыдущих главах, нам удобно будет использовать Rake задачу для заполнения базы данных образцами взаимоотношений. Это позволит нам заняться в первую очередь внешним видом страниц, временно отложив прикладную часть функциональности.
Когда мы в последний раз видели заполнитель образцов данных в Листинге 11.20, он был немного суматошным. Поэтому мы начнем с определения отдельных методов для создания пользователей и микросообщений, а затем добавим образцы данных взаимоотношений используя новый метод make_relationships. Результаты показаны в Листинге 12.18.
Листинг 12.18. Добавление читатели/читающие взаимоотношений в образцы данных.
lib/tasks/sample_data.rake
namespace :db do
  desc "Fill database with sample data"
  task :populate => :environment do
    Rake::Task['db:reset'].invoke
    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.toggle!(:admin)
  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
  User.all(:limit => 6).each do |user|
    50.times do
      content = Faker::Lorem.sentence(5)
      user.microposts.create!(:content => content)
    end
  end
end

def make_relationships
  users = User.all
  user  = users.first
  following = users[1..50]
  followers = users[3..40]
  following.each { |followed| user.follow!(followed) }
  followers.each { |follower| follower.follow!(user) }
end
Здесь образцы взаимоотношений создаются с помощью кода
def make_relationships
  users = User.all
  user  = users.first
  following = users[1..50]
  followers = users[3..40]
  following.each { |followed| user.follow!(followed) }
  followers.each { |follower| follower.follow!(user) }
end
Мы несколько произвольно организовали слежение первого пользователя за сообщениями следующих 50 пользователей, а затем принудили пользователей с id от 4 до 41 читать сообщения первого пользователя. Полученных в результате взаимоотношений будет вполне достаточно для разработки интерфейса приложения.
Чтобы выполнить код Листинга 12.18, заполним базу данных как обычно:
$ bundle exec rake db:populate

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

Теперь, когда у наших образцов пользователей есть массивы читателей и читающих, нам нужно обновить главные страницы и страницы профилей, чтобы отразить это. Мы начнем с создания партиала для отображения статистики читаемых и читателей на странице профиля и на главной странице, как показано на макетах в Рис. 12.1 и Рис. 12.5. Результат будет отображать количиство читаемых и читающих пользователей, вместе со ссылками на специальные страницы для их просмотра. Затем мы добавим читать/не читать форму, и сделаем отдельные страницы для отображения читаемых и читающих пользователей.
stats_partial_mockup
Рисунок 12.10: Макет для партиала статистики.
Крупный план области статистики, взятый из макета на Рис. 12.1, представлен в Рис. 12.10. Эта статистика содержит и количество читаемых пользователей, и количество его читателей, каждое из чисел должно быть ссылкой на соответствующую специальную страницу для их отображения. В Главе 5, мы временно заглушали подобные ссылки знаком ’#’, но это было до того, как мы набрались опыта с маршрутами. Сейчас, несмотря на то, что мы отложили сами страницы до Раздела 12.2.3, мы сделаем маршруты сейчас, как показано в Листинге 12.19. Этот код использует метод :member внутри блока resources, с которым мы ранее не были знакомы, но посмотрим, сможете ли вы угадать, что он делает.
Листинг 12.19. Добавление действий 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, и это именно то, что делает код в Листинге 12.19. Поскольку обе страницы будут отображать данные, мы используем get для того чтобы организовать ответ URL на запросы GET (что требуется конвенцией REST для подобных страниц), и метод member означает, что маршруты отвечают на URL, содержащие id пользователя. (Другой возможный метод, collection, работает без id, так что
resources :users do
  collection do
    get :tigers
  end
end
будет отвечать на URL /users/tigers — предположительно для отображения всех тигров нашего приложения. Узнать больше об этой опции можно из Ruby on Rails по-русски “Роутинг в Rails”.) Таблица маршрутов, сгенерированных Листингом 12.19 представлена в Таблице 12.1; обратите внимание на именнованные маршруты для страниц с читателями и читающими, которые мы сейчас будем использовать.
HTTP запросURLДействиеИменованный маршрут
GET/users/1/followingfollowingfollowing_user_path(1)
GET/users/1/followersfollowersfollowers_user_path(1)
Таблица 12.1: RESTful маршруты обеспеченные пользовательскими правилами ресурса из Листинга 12.19.
Определив маршруты мы готовы сделать тесты партиала статистики. (Мы могли бы начать с тестов, но именованные маршруты было бы трудно to motivate(??) без обновленного файла маршрутов.) Мы могли бы написать тесты для страницы профиля пользователя, поскольку партиал статистики присутствует на ней, но он также представлен и на Home странице, и это замечательная возможность для рефакторинга тестов Home страницы, принимая во внимание вошедших пользователей. Результат представлен в Листинге 12.20.
Листинг 12.20. Тестирование статистики читатели/читающие на Home странице.
spec/controllers/pages_controller_spec.rb
describe PagesController do
  render_views

  before(:each) do
    @base_title = "Ruby on Rails Tutorial Sample App"
  end
  .
  .
  .
  describe "GET 'home'" do

    describe "when not signed in" do

      before(:each) do
        get :home
      end

      it "should be successful" do
        response.should be_success
      end

      it "should have the right title" do
        response.should have_selector("title",
                                      :content => "#{@base_title} | Home")
      end
    end

    describe "when signed in" do

      before(:each) do
        @user = test_sign_in(Factory(:user))
        other_user = Factory(:user, :email => Factory.next(:email))
        other_user.follow!(@user)
      end

      it "should have the right follower/following counts" do
        get :home
        response.should have_selector("a", :href => following_user_path(@user),
                                           :content => "0 following")
        response.should have_selector("a", :href => followers_user_path(@user),
                                           :content => "1 follower")
      end
    end
  end
end
Ядром этих тестов является предположение, что количество читателей и читаемых представлено на странице совместно с правильными URL:
response.should have_selector("a", :href => following_user_path(@user),
                                   :content => "0 following")
response.should have_selector("a", :href => followers_user_path(@user),
                                   :content => "1 follower")
Здесь мы использовали именованные маршруты, показанные в Таблице 12.1 для проверки того, что ссылки имеют правильные URL.
Код приложения для партиала статистики это всего лишь таблица внутри div, как показано в Листинге 12.21.
Листинг 12.21. Партиал для отображения статистики.
app/views/shared/_stats.html.erb
<% @user ||= current_user %>
<div class="stats">
  <table summary="User stats">
    <tr>
      <td>
        <a href="<%= following_user_path(@user) %>">
          <span id="following" class="stat">
            <%= @user.following.count %> following
          </span>
        </a>
      </td>
      <td>
        <a href="<%= followers_user_path(@user) %>">
          <span id="followers" class="stat">
            <%= pluralize(@user.followers.count, "follower") %>
          </span>
        </a>
      </td>
    </tr>
  </table>
</div>
Здесь количество читателей и читаемых подсчитывается через ассоциации с использованием
@user.following.count
и
@user.followers.count
Сравните это с подсчетом микросообщений из Листинга 11.16, где мы писали
@user.microposts.count
для количества микросообщений.
Поскольку мы будем включать статистику и в страницу профиля пользователя и в главную страницу, первая строка Листинга 12.21 выбирает правильное используя
<% @user ||= current_user %>
Как обсуждалось в Блоке 9.4, это ничего не делает когда @user не является nil (как на странице профиля), но в противном случае (как на Home странице) это устанавливает @user к текущему пользователю.
Одна последняя деталь, которую стоит отметить, это наличие CSS id в некоторых элементах, как в
<span id="following" class="stat">
...
</span>
Это сделано в угоду Ajax реализации из Раздела 12.2.5, которая получает доступ к элементам страницы используя их уникальные id.
С готовым партиалом включить статистику в Home страницу проще простого, как показано в Листинге 12.22. (Это также приводит к прохождению тестов из Листинга 12.20.) результат представлен в Рис. 12.11.
Листинг 12.22. Добавление статистики к Home странице.
app/views/pages/home.html.erb
<% if signed_in? %>
        .
        .
        .
        <%= render 'shared/user_info' %>
        <%= render 'shared/stats' %>
      </td>
    </tr>
  </table>
<% else %>
  .
  .
  .
<% end %>
home_page_follow_stats
Рисунок 12.11: Home страница (/) со статистикой. (полный размер)
Мы подключим статистику к странице профиля через мгновение, но вначале давайте сделааем партиал для follow/unfollow кнопки, как показано в Листинге 12.23.
Листинг 12.23. Партиал для формы follow/unfollow.
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 (Листинг 11.21), как показано в Листинге 12.24.
Листинг 12.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 показаны в Листинге 12.25 и Листинге 12.26.
Листинг 12.25. Форма для слежения за сообщениями пользователя.
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>
  <div class="actions"><%= f.submit "Follow" %></div>
<% end %>
Листинг 12.26. Форма для отписки от сообщений пользователя.
app/views/users/_unfollow.html.erb
<%= form_for current_user.relationships.find_by_followed_id(@user),
             :html => { :method => :delete } do |f| %>
  <div class="actions"><%= f.submit "Unfollow" %></div>
<% end %>
Обе эти формы используют form_for для манипуляций с объектом модели Relationship; основное отличие между ними заключается в том, что Листинг 12.25 строит новое взаимоотношение, тогда как Листинг 12.26 ищет существующее взаимоотношение. Естественно, первый отправляет POST запрос к контроллеру Relationships для create взаимоотношения, в то время как последний отправляет DELETE запрос для destroy взаимоотношения. (Мы напишем эти действия в Разделе 12.2.4.) Наконец, отметьте, что форма follow/unfollow не содержит никакого контента кроме кнопки, но нам все еще необходимо отправить followed_id, чего мы можем добиться с помощью hidden_field; который производит HTML формы
<input id="relationship_followed_id" name="relationship[followed_id]"
type="hidden" value="3" />
которая поместит соответствующую информацию не отображая ее в браузере.
Теперь мы можем включить форму чтения и статистику на страницу профиля пользователя простым рендерингом партиалов, как показано в Листинге 12.27. Профили с follow и unfollow кнопками, соответственно, представлены на Рис. 12.12 и Рис. 12.13.
Листинг 12.27. Добавление формы слежения за сообщениями пользователя и статистики на страницу профиля пользователя.
app/views/users/show.html.erb
<table class="profile" summary="Profile information">
  <tr>
    <td class="main">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
      <%= render 'follow_form' if signed_in? %>
      .
      .
      .
    </td>
    <td class="sidebar round">
      <strong>Name</strong> <%= @user.name %><br />
      <strong>URL</strong> <%= link_to user_path(@user), @user %><br />
      <strong>Microposts</strong> <%= @user.microposts.count %>
      <%= render 'shared/stats' %>
    </td>
  </tr>
</table>
profile_follow_button
Рисунок 12.12: Профиль пользователя с follow кнопкой (/users/8). (полный размер)
profile_unfollow_button
Рисунок 12.13: Профиль пользователя с unfollow кнопкой (/users/6). (полный размер)
Мы вскоре сделаем эти кнопки рабочими, фактически, мы сделаем это двумя способами, стандартным способом (Раздел 12.2.4) и с использованием Ajax (Раздел 12.2.5), но вначале мы закончим HTML интерфейс, создав страницы для списков читающих и читаемых.

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

Страницы для отображения читающих сообщения пользователя и читаемых им пользователей будут напоминать гибрид страницы профиля пользователя и страницы со списком пользователей (Раздел 10.3.1), с сайдбаром пользовательской информации (включая статистику слежения за сообщениями) и таблицу пользователей. Кроме того, мы включим a raster (??) пользовательских профильных изображений-ссылок в сайдбаре. Макет соответствующий этим требованиям представлен в Рис. 12.14 (читаемые) и Рис. 12.15 (читатели).
following_mockup
Рисунок 12.14: Макет страницы со списком читаемых пользователей. (полный размер)
followers_mockup
Рисунок 12.15: Макет страницы со списком читателей. (полный размер)
Нашим первым шагом будет получение рабочих ссылок following (читаемые) и followers (читатели). Мы будем следовать примеру Твиттера и обе страницы будут требовать входа пользователя. Для вошедших пользователей, страницы должны иметь ссылки на читаемых и читателей, соответственно. Листинг 12.28 выражает эти ожидания в коде.12
Листинг 12.28. Тесты для действий following и followers.
spec/controllers/users_controller_spec.rb
describe UsersController do
  .
  .
  .
  describe "follow pages" do

    describe "when not signed in" do

      it "should protect 'following'" do
        get :following, :id => 1
        response.should redirect_to(signin_path)
      end

      it "should protect 'followers'" do
        get :followers, :id => 1
        response.should redirect_to(signin_path)
      end
    end

    describe "when signed in" do

      before(:each) do
        @user = test_sign_in(Factory(:user))
        @other_user = Factory(:user, :email => Factory.next(:email))
        @user.follow!(@other_user)
      end

      it "should show user following" do
        get :following, :id => @user
        response.should have_selector("a", :href => user_path(@other_user),
                                           :content => @other_user.name)
      end

      it "should show user followers" do
        get :followers, :id => @other_user
        response.should have_selector("a", :href => user_path(@user),
                                           :content => @user.name)
      end
    end
  end
end
Единственно сложная часть реализации это осуществление потребности добавления двух новых действий к контроллеру Users; основанных на маршрутах, определенных в Листинге 12.19, нам нужно назвать их following и followers. Каждому действию нужно установить заголовок, найти пользователя, вытянуть @user.following или @user.followers (в пагинированной форме), а затем отренедерить страницу. Результат представлен в Листинге 12.29.
Листинг 12.29. Действия following и followers.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_filter :authenticate, :except => [:show, :new, :create]
  .
  .
  .
  def following
    @title = "Following"
    @user = User.find(params[:id])
    @users = @user.following.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
  .
  .
  .
end
Отметим здесь, что оба действия делают явный вызов render, в данном случае делая рендеринг представления, названного show_follow, которое мы должны создать. Причина создания общего представления в том, что ERb является практически идентичным для обоих случаев, и Листинг 12.30 охватывает их.
Листинг 12.30. Представление show_follow используемое для рендеринга читаемых и читателей.
app/views/users/show_follow.html.erb
<table summary="Information about following/followers">
  <tr>
    <td class="main">
      <h1><%= @title %></h1>

      <% unless @users.empty? %>
        <ul class="users">
          <%= render @users %>
        </ul>
        <%= will_paginate @users %>
      <% end %>
    </td>
    <td class="sidebar round">
      <strong>Name</strong> <%= @user.name %><br />
      <strong>URL</strong> <%= link_to user_path(@user), @user %><br />
      <strong>Microposts</strong> <%= @user.microposts.count %>
      <%= render 'shared/stats' %>
      <% unless @users.empty? %>
        <% @users.each do |user| %>
          <%= link_to gravatar_for(user, :size => 30), user %>
        <% end %>
      <% end %>
    </td>
  </tr>
</table>
Есть еще одна деталь, которую стоит отметить в Листинге 12.29: в целях защиты страниц для читаемых и читателей от неавторизированного доступа мы заменили в аутентификационном предфильтре :only на :except. До сих пор в этом учебнике мы использовали :only для указания к каким действиям применяется фильтр; с добавлением новых защищаемых действий баланс сместился и стало проще указать какие действия не должны быть отфильтрованы. Мы сделли это с :except опцией предфильтра authenticate:
before_filter :authenticate, :except => [:show, :new, :create]
Теперь тесты должны пройти, и страницы должны рендериться как показано на Рис. 12.16 (читаемые) и Рис. 12.17 (читатели).
user_following
Рисунок 12.16: Отображение читаемых пользователей. (полный размер)
user_followers
Рисунок 12.17: Отображение читателей текущего пользователя. (полный размер)
Вы могли заметить, что даже с общим партиалом show_followers, following и followers действия все еще содержат много повторений. Кроме того, сам партиал show_followers разделяет общие функции со страницей отбражающей пользователей. Раздел 12.5 включает упражнения для устранения этих источников дублированного кода.

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

Теперь когда наши представления в порядке, пришло время получить рабочие follow/unfollow кнопки. Поскольку чтение сообщений пользователя создает взаимоотношение, а отписка от сообщений пользователя разрушает взаимоотношение, это предполагает написание create и destroy действий для контроллера Relationships. Естественно, оба действия должны быть защишены; для вошедших пользователей мы будем использовать follow! и unfollow! служебные методы, определенные в Разделе 12.1.4 для создания и разрушения соответствующих взаимоотношений. Эти требования приводят к тестам в Листинге 12.31.
Листинг 12.31. Тесты для действий контроллера Relationships.
spec/controllers/relationships_controller_spec.rb
require 'spec_helper'

describe RelationshipsController do

  describe "access control" do

    it "should require signin for create" do
      post :create
      response.should redirect_to(signin_path)
    end

    it "should require signin for destroy" do
      delete :destroy, :id => 1
      response.should redirect_to(signin_path)
    end
  end

  describe "POST 'create'" do

    before(:each) do
      @user = test_sign_in(Factory(:user))
      @followed = Factory(:user, :email => Factory.next(:email))
    end

    it "should create a relationship" do
      lambda do
        post :create, :relationship => { :followed_id => @followed }
        response.should be_redirect
      end.should change(Relationship, :count).by(1)
    end
  end

  describe "DELETE 'destroy'" do

    before(:each) do
      @user = test_sign_in(Factory(:user))
      @followed = Factory(:user, :email => Factory.next(:email))
      @user.follow!(@followed)
      @relationship = @user.relationships.find_by_followed_id(@followed)
    end

    it "should destroy a relationship" do
      lambda do
        delete :destroy, :id => @relationship
        response.should be_redirect
      end.should change(Relationship, :count).by(-1)
    end
  end
end
Отметьте здесь как
:relationship => { :followed_id => @followed }
симулирует отправку формы со скрытыми полями, обеспеченной кодом
<%= f.hidden_field :followed_id %>
Код контроллера, необходимый для прохождения этих тестов удивительно краток: мы просто вытягиваем читаемого или читающего пользователя, а затем читаем или не читаем его сообщения используя соответствующий служебный метод. Полная реализация представлена в Листинге 12.32.
Листинг 12.32. Контроллер Relationships.
app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_filter :authenticate

  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
С этим ядро функциональности follow/unfollow завершено, и любой пользователь может читать (или не читать) сообщения любого другого пользователя.

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

Хотя наша реализация слежения за сообщениями пользователей является законченной и в своем нынешнем виде, нам осталось совсем немного подправить ее прежде чем заняться лентой сообщений. Вы могли заметить в Разделе 12.2.4 что оба create и destroy действия в контроллере Relationships просто перенаправляют обратно к исходному профилю. Другими словами, пользователь начинает на странице профиля, подписывается на сообщения пользователя и немедленно перенаправляется на исходную страницу. Резонно спросить - почему пользователь вообще должен покидать эту страницу?
Именно эту проблему решает Ajax, который позволяет веб страницам отправлять асинхронные запросы на сервер не покидая страницы.13 Поскольку практика добавления Ajax в веб формы является довольно распространенной, Rails делает реализацию Ajax легкой. Действительно, обновление партиалов формы follow/unfollow тривиально: просто заменим
form_for
на
form_for ..., :remote => true
и Rails автомагически будет использовать Ajax.14 Обновленные партиалы представлены в Листинге 12.33 и Листинге 12.34.
Листинг 12.33. Форма для чтения сообщений пользователя использующая Ajax.
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>
  <div class="actions"><%= f.submit "Follow" %></div>
<% end %>
Листинг 12.34. Форма для прекращения чтения сообщений пользователя использующая Ajax.
app/views/users/_unfollow.html.erb
<%= form_for current_user.relationships.find_by_followed_id(@user),
             :html => { :method => :delete },
             :remote => true do |f| %>
  <div class="actions"><%= f.submit "Unfollow" %></div>
<% 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 является довольно сложным, и тщательное делание этого является большой темой со своими собственными правилами, но мы можем начать с кодом в Листинге 12.35. Это использует xhr метод (от “XmlHttpRequest”) для выдачи Ajax запроса; сравните с get, post, put и delete методами в предыдущих тестах. Затем мы проверяем что create и destroy действия делают правильные вещи когда вызываются Ajax запросом. (Для написания более основательного набора тестов для насыщенных Ajax-ом приложений, взгляните на Selenium и Watir.)
Листинг 12.35. Тесты для ответов контроллера Relationships на Ajax запросы.
spec/controllers/relationships_controller_spec.rb
describe RelationshipsController do
  .
  .
  .
  describe "POST 'create'" do
    .
    .
    .
    it "should create a relationship using Ajax" do
      lambda do
        xhr :post, :create, :relationship => { :followed_id => @followed }
        response.should be_success
      end.should change(Relationship, :count).by(1)
    end
  end

  describe "DELETE 'destroy'" do
    .
    .
    .
    it "should destroy a relationship using Ajax" do
      lambda do
        xhr :delete, :destroy, :id => @relationship
        response.should be_success
      end.should change(Relationship, :count).by(-1)
    end
  end
end
Как следует из тестов, код приложения использует те же create и delete действия для ответа на Ajax запросы которые он выдает чтобы ответить на обычные POST и DELETE HTTP запросы. Все что нам нужно сделать это ответить на обычный HTTP запрос с переадресацией (как в Разделе 12.2.4) и ответить на Ajax запрос с JavaScript.15 Код контроллера представлен в Листинге 12.36. (См. в Разделе 12.5 пример, показывающий более компактный способ выполнить то же самое.)
Листинг 12.36. Ответ на Ajax запросы в контроллере Relationships.
app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_filter :authenticate

  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 чтобы принять соответствующее действие в зависимости от вида запроса.16 Синтаксис может запутать и важно понимать, что в
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 автоматически обеспечивает Prototype JavaScript хелперы для манипуляции страницей при помощи Document Object Model (DOM). Prototype обеспечивает большое количество методов для манипуляции DOM, но здесь нам понадобятся только два. Во первых мы должны знать о синтаксисе знака доллара, используемого Prototype для доступа к DOM элементу, основанному на его уникальном CSS id. Например, для манипуляции элементом follow_form, мы используем синтаксис
$("follow_form")
(Вспомните из Листинга 12.23 что это div который обертывает форму, а не сама форма.) Второй метод который нам потребуется это update, который обновляет HTML внутри соответствующего элемента содержимым своего аргумента. Например, чтобы полностью заменить follow form на строку "foobar", мы можем написать
$("follow_form").update("foobar")
В отличие от простых JavaScript файлов, JS-ERb файлы позволяют также использовать Embedded Ruby, что мы применяем в create.js.erb файле для обновления в форме unfollow партиала (это то, что должно быть видно после успешного following) и обновления количества читаемых. Результат представлен в Листинге 12.37.
Листинг 12.37. JavaScript Embedded Ruby для создания взаимоотношения при чтении сообщений другого пользователя.
app/views/relationships/create.js.erb
$("follow_form").update("<%= escape_javascript(render('users/unfollow')) %>")
$("followers").update('<%= "#{@user.followers.count} followers" %>')
Файл destroy.js.erb аналогичен (Листинг 12.38). Отметьте, что как и в Листинге 12.37, мы должны использовать escape_javascript для маскирования результатов при вставке HTML.
Листинг 12.38. Ruby JavaScript (RJS) для уничтожения взаимоотношения при чтении сообщений другого пользователя.
app/views/relationships/destroy.js.erb
$("follow_form").update("<%= escape_javascript(render('users/follow')) %>")
$("followers").update('<%= "#{@user.followers.count} followers" %>')
Теперь вам следует перейти на страницу профиля пользователя и проверить, что вы можете следить и не следить за сообщениями пользователей без обновления страницы.
Использование Ajax в Rails является большой и стремительно развивающейся темой, и здесь мы лишь слегка коснулись ее, но (как и с остальными материалами в этом учебнике) наш подход дает вам хорошую основу для изучения более продвинутых ресурсов. Особенно стоит отметить, что, в дополнение к Prototype, JavaScript фреймворк jQuery получил большое признание в Rails сообществе. Фактически, как обсуждается в Главе 13, в Rails 3.1 jQuery является дефолтным фреймворком. Реализация Ajax функций из этого раздела с использованием jQuery оставлена в качестве упражнения; см. Раздел 12.5 и особенно Раздел 13.1.4.2.

12.3 Лента сообщений

Мы подошли к кульминации нашего примера приложения: ленте сообщений. Соответствено, этот раздел содержит некоторые из самых продвинутых материалов в данном учебнике. Создание ленты сообщений подразумевает сборку массива микросообщений из микросообщений читаемых пользователей, совместно с собственными микросообщениями текущего пользователя. Чтобы совершить этот подвиг, нам понадобятся некоторые довольно продвинутые Rails, Ruby и даже SQL техники программирования.
Так как нам предстоит тяжелая работа, особенно важно понимать куда мы будем двигаться. Макет финальной ленты сообщений пользователя, которая основана на предварительной реализации потока сообщений из Раздела 11.3.3, представлен в Рис. 12.18.
home_page_feed_mockup
Рисунок 12.18: Макет Home страницы пользователя с лентой сообщений. (полный размер)

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

Основная идея потока сообщений проста. Рис. 12.19 показывает пример таблицы базы данных microposts и результирующий поток сообщений. Цель потока заключается в вытягивании микросообщений чей user id соответствует пользователям, сообщения которых читает текущий пользователь (и id самого текущего пользователя), как указано стрелками на схеме.
user_feed
Рисунок 12.19: Поток сообщений для пользователя (id 1) читающего сообщения пользователей 2, 7, 8 и 10.
Поскольку нам необходим способ найти все микросообщения пользователей, за которыми следит данный пользователь, мы запланируем реализацию метода называемого from_users_followed_by, который мы будем использовать следующим образом:
Micropost.from_users_followed_by(user)
Хотя мы пока не знаем как реализовать его, мы уже можем написать тесты для from_users_followed_by, как показано в Листинг 12.39.
Листинг 12.39. Тесты для Micropost.from_users_followed_by.
spec/models/micropost_spec.rb
describe Micropost do
  .
  .
  .
  describe "from_users_followed_by" do

    before(:each) do
      @other_user = Factory(:user, :email => Factory.next(:email))
      @third_user = Factory(:user, :email => Factory.next(:email))

      @user_post  = @user.microposts.create!(:content => "foo")
      @other_post = @other_user.microposts.create!(:content => "bar")
      @third_post = @third_user.microposts.create!(:content => "baz")

      @user.follow!(@other_user)
    end

    it "should have a from_users_followed_by class method" do
      Micropost.should respond_to(:from_users_followed_by)
    end

    it "should include the followed user's microposts" do
      Micropost.from_users_followed_by(@user).should include(@other_post)
    end

    it "should include the user's own microposts" do
      Micropost.from_users_followed_by(@user).should include(@user_post)
    end

    it "should not include an unfollowed user's microposts" do
      Micropost.from_users_followed_by(@user).should_not include(@third_post)
    end
  end
end
Ключевым моментом здесь является построение ассоциаций в блоке before(:each) и последующая проверка всех трех требований: микросообщения читаемых пользователей и самого пользователя включены, но сообщений нечитаемых пользователей нет.
Сам поток сообщений живет в модели User (Раздел 11.3.3), так что мы должны добавить дополнительный тест в User model specs из Листинга 11.31, как показано в Листинге 12.40. (Отметьте, что мы здесь переключились с использования include?, как показано в Листинге 11.31, к более компактной include конвенции, введеной в Листинге 12.12.)
Листинг 12.40. Финальные тесты для ленты сообщений.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "micropost associations" do
    .
    .
    .
    describe "status feed" do

      it "should have a feed" do
        @user.should respond_to(:feed)
      end

      it "should include the user's microposts" do
        @user.feed.should include(@mp1)
        @user.feed.should include(@mp2)
      end

      it "should not include a different user's microposts" do
        mp3 = Factory(:micropost,
                      :user => Factory(:user, :email => Factory.next(:email)))
        @user.feed.should_not include(mp3)
      end

      it "should include the microposts of followed users" do
        followed = Factory(:user, :email => Factory.next(:email))
        mp3 = Factory(:micropost, :user => followed)
        @user.follow!(followed)
        @user.feed.should include(mp3)
      end
    end
    .
    .
    .
  end
end
Реализация потока сообщений будет легкой; мы просто отложим ее до Micropost.from_users_followed_by, как показано в Листинге 12.41.
Листинг 12.41. Добавление завершенного потока к модели User.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def feed
    Micropost.from_users_followed_by(self)
  end
  .
  .
  .
end

12.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, что позволяет нам протестировать множественное включение. (К счастью это так.)
Вспомним из предварительной реализации потока в Разделе 11.3.3 что Active Record использует where метод для осуществления вида выбора, показанного выше, что иллюстрирует Листинг 11.32. Там наш выбор был очень прост; мы просто взяли все микросообщения с user id соответствующим текущему пользователю:
Micropost.where("user_id = ?", id)
Здесь мы ожидаем, что он будет более сложным, чем то вроде
where("user_id in (#{following_ids}) OR user_id = ?", user)
(Здесь мы в состоянии использовать, согласно Rails конвенции, user вместо user.id; Rails автоматически использует id. Мы также опустили впереди идущую часть Micropost. поскольку мы ожидаем что этот метод будет жить в самой модели Micropost.)
Мы видим из этих условий, что нам нужен массив id пользователей, читаемых данным пользователем (или какой-то эквивалент). Один из способов сделать это заключается в использовании Ruby метода map, доступного на любом “перечисляемом” объекте, т.е., любом объекте (таком как Массив или Хэш), который состоит из коллекции элементов.17 Мы видели пример этого метода в Разделе 4.3.2; он работает следующим образом:
$ rails console
>> [1, 2, 3, 4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]
Ситуации, подобные той, что показана выше, где такой же метод (например, to_s) вызывается на каждый элемент, настолько обычная вещь, что есть сокращенная запись, использующая ампресанд & и символ, соответствующий методу:18
>> [1, 2, 3, 4].map(&:to_s)
=> ["1", "2", "3", "4"]
Мы можем использовать эту запись для конструирования необходимого массива id читаемых пользователей, вызвав id на каждом элементе в user.following. Например, для первого пользователя в базе данных этот массив выглядит следующим образом:
>> User.first.following.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.following_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]
В этой точке вы можете догадаться, что код вида
Micropost.from_users_followed_by(user)
будет включать в себя метод класса в Micropost классе (конструкция в последний раз виденная в User классе в Листинге 7.12). Предполагаемая реализация с этими строками представлена в Листинге 12.42.
Листинг 12.42. Первый дубль для from_users_followed_by метода.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  def self.from_users_followed_by(user)
    following_ids = user.following_ids
    where("user_id IN (#{following_ids}) OR user_id = ?", user)
  end
end
Хотя обсуждение ведущее к Листингу 12.42 было выдержано в гипотетических тонах, он действительно работает! Фактически, этого было бы достаточно для большинства практических целей. Но это не финальная реализация; посмотрим, сможете ли вы догадаться почему, прежде чем перейти к следующему разделу. (Намек: А что если пользователь следит за сообщениями 5000 других пользователей?)
Кстати, если бы мы использовали метод из Листинга 12.42 для финальной реализации, мы могли бы сделать отличный рефакторинг. Метод where может принимать хэш аргументов с ключом :user_id и значением эквивалентным массиву пользователей. В данном случае нам необходим массив включающий в себя всех читаемых пользователей, а также самого пользователя; мы можем организовать это используя push метод (упоминался в Разделе 4.3.1) на following ассоциации, как видно в Листинге 12.42.
Листинг 12.43. Реорганизованный метод from_users_followed_by.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  def self.from_users_followed_by(user)
    where(:user_id => user.following.push(user))
  end
end

12.3.3 Пространства, подзапросы и лямбда

Как намекалось в последнем разделе, реализация потока сообщений в Разделе 12.3.2 не очень хорошо масштабируется при большом количестве микросообщений, что скорее всего произойдет если пользователь начнет читать сообщения, скажем, 5000 других пользователей. В этом разделе мы повторно реализуем ленту сообщений способом, который лучше масштабируется с количеством читаемых пользователей.
Есть несколько проблем с кодом в Разделе 12.3.2. Во-первых, выражение
following_ids = user.following_ids
вытягивает всех читаемых пользователей в память и создает массив длинной во весь список читаемых пользователей. Поскольку условие в Листинге 12.42 на самом деле лишь проверяет включение множества, должен быть более эффективный способ для этого, да и SQL оптимизирован именно для таких множественных операций. Во-вторых, метод в Листинге 12.42 всегда вытягивает все микросообщения и вколачиват их в Ruby массив. Хотя эти микросообщения пагинированы в представлении (Листинг 11.33), массив по-прежнему полноразмерный.19 Что мы действительно хотим, так это честную пагинацию, которая вытягивает только 30 элементов за раз.
Решение обеих задач включает в себя преобразование потока сообщений из метода класса в пространство, которое является Rails методом для ограничения выборки из базы данных, основанном на определенных условиях. Например, чтобы организовать выборку методом scope всех административных пользователей в нашем приложении, мы могли бы добавить пространство к User модели следующим образом:
class User < ActiveRecord::Base
  .
  .
  .
  scope :admin, where(:admin => true)
  .
  .
  .
end
В результате этого пространства, код
User.admin
вернет массив всех администраторов сайта.
Основная причина, по которой пространства лучше подходят, чем обычные методы класса заключается в том, что они могут быть сцеплены с другими методами, так что, например,
User.admin.paginate(:page => 1)
фактически пагинирует администраторов в базе данных; если (по какой-то странной причине) сайт имеет 100 администраторов, код выше по-прежнему выведет первые 30.
пространство для потока сообщений немногим более сложно чем проиллюстрированное выше: ему нужен аргумент, а именно, пользователь, чей поток нам необходимо генерировать. Мы можем сделать это с анонимной функцией, или lambda (обсуждалось в Разделе 8.4.2), как показано в Листинге 12.44.20
Листинг 12.44. Улучшение from_users_followed_by.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  default_scope :order => 'microposts.created_at DESC'

  # Return microposts from the users being followed by the given user.
  scope :from_users_followed_by, lambda { |user| followed_by(user) }

  private

    # Return an SQL condition for users followed by the given user.
    # We include the user's own id as well.
    def self.followed_by(user)
      following_ids = user.following_ids
      where("user_id IN (#{following_ids}) OR user_id = :user_id",
            { :user_id => user })
    end
end
Поскольку условия на from_users_followed_by scope довольно длинные, мы определили вспомогательную функцию для его обработки:
def self.followed_by(user)
  following_ids = user.following_ids
  where("user_id IN (#{following_ids}) OR user_id = :user_id",
        { :user_id => user })
end
В качестве подготовки к следующему шагу, мы заменили
where("... OR user_id = ?", user)
на эквивалентное
where("... OR user_id = :user_id", { :user_id => user })
Синтаксис со знаком вопроса это хорошо, но когда мы хотим ту же переменную видеть вставленной в более чем одном месте, второй вариант синтаксиса, использующий хэш является более удобным.
Из вышеизложенного следует, что мы будем добавлять второе вхождение user_id в SQL запросе, и это действительно так. Мы можем заменить Ruby код
following_ids = user.following_ids
на фрагмент SQL
following_ids = %(SELECT followed_id FROM relationships
                 WHERE follower_id = :user_id)
(См. в Блоке 12.1 объяснение %() синтаксиса.) Этот код содержит SQL подзапрос, и внутренне вся выборка для пользователя 1 будет выглядеть подобным образом:
SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
                  WHERE follower_id = 1)
      OR user_id = 1
Этот подзапрос организует набор логических выражений для отправки в базу данных, что является более эффективным.21
С этим фундаментом мы готовы к эффективной релизации потока сообщений, как видно в Листинге 12.45.
Листинг 12.45. Финальная реализация from_users_followed_by.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  default_scope :order => 'microposts.created_at DESC'

  # Return microposts from the users being followed by the given user.
  scope :from_users_followed_by, lambda { |user| followed_by(user) }

  private

    # Return an SQL condition for users followed by the given user.
    # We include the user's own id as well.
    def self.followed_by(user)
      following_ids = %(SELECT followed_id FROM relationships
                        WHERE follower_id = :user_id)
      where("user_id IN (#{following_ids}) OR user_id = :user_id",
            { :user_id => user })
    end
end
Этот код представляет собой внушительную комбинацию Rails, Ruby, и SQL, но он делает свою работу и делает ее хорошо.22

12.3.4 Новая лента сообщений

С кодом в Листинге 12.45, наша лента сообщений завершена. Напомним, что код для Home страницы, представлен в Листинге 12.46; этот код создает пагинированный поток соответствущих микросообщений для использования в представлении, как видно в Рис. 12.20.23 Отметим, что paginate метод фактически достигает цели в методе модели Micropost в Листинге 12.45, организуя вытягивание только 30 микросообщений за раз из базы данных.24
Листинг 12.46. Home действие с пагинированным потоком.
app/controllers/pages_controller.rb
class PagesController < ApplicationController

  def home
    @title = "Home"
    if signed_in?
      @micropost = Micropost.new
      @feed_items = current_user.feed.paginate(:page => params[:page])
    end
  end
  .
  .
  .
end
home_page_with_feed
Рисунок 12.20: Home страница с работающей лентой сообщений. (полный размер)

12.4 Заключение

Добавив ленту сообщений, мы закончили ядро примера приложения Учебника Ruby on Rails. Это приложение включает в себя примеры всех основных возможностей Rails, включая модели, представления, контроллеры, шаблоны, партиалы, фильтры, валидации, обратные вызовы, has_many/belongs_to и has_many :through ассоциации, безопасность, тестирование и развертывание. Несмотря на это внушительный список, предстоит еще многое узнать о Rails. В качестве первого шага в этом процессе, этот раздел содержит некоторые рекомендуемые расширения основного приложения, а также рекомендации для дальнейшего обучения.
Прежде чем перейти к решению любого из предложенных расширений приложения, хорошо бы объединить ваши изменения и развернуть приложение:
$ git add .
$ git commit -m "Added user following"
$ git checkout master
$ git merge following-users
$ git push heroku
$ heroku rake db:migrate

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

Рекомендуемые расширения в этом разделе в основном вдохновлены основными функциями, общепринятыми для веб-приложений, такими как напоминание пароля и подтверждение адреса электронной почты, или функции характерные для нашего типа примера приложения, такие как поиск, ответы, обмен сообщениями. Реализация одного или более расширений приложения поможет вам сделать переход от выполнения примеров учебника к написанию собственных оригинальных приложений.
Не удивляйтесь, если по началу будет непросто; чистый лист новой функции может быть немного пугающим. Чтобы помочь вам начать, я могу дать две большие рекомендации. Во-первых, пежде чем добавлять какую-либо функцию к Rails приложению, взгляните на Railscasts, все эпизоды чтобы посмотреть, не рассказал ли уже об этом Ryan Bates.25 Если он это сделал, просмотр соответствующего Railscast сэкономит вам массу времени. Во-вторых, всегда делайте обширный поиск в Google по вашей предполагаемой функции, чтобы найти соответствующее сообщения в блогах и пособиях. Разработка веб приложений это непростое дело и это поможет вам учиться на чужом опыте (и ошибках).
Многие из следующих функций являются довольно непростыми задачами, и я дал несколько подсказок по поводу средств, которые могут вам понадобиться для их реализации. Даже с подсказками, они являются намного более трудными чем упражнения, которые приводились в конце каждой главы учебника, так что не расстраивайтесь, если вы не можете решить их без значительных усилий. Из-за нехватки времени я недоступен для личной помощи, но если есть достаточный интерес я мог бы выпустить автономную статью/скринкаст охватывающий эти расширения в будущем; перейдите на основной сайт Rails Tutorial http://www.railstutorial.org/ и подпишитесь на ленту новостей, чтобы быть в курсе последних обновлений.

Реплики

Твиттер позволяет пользователям делать “@replies”, которые являются микросообщениями, чьи первые символы являются логином пользователя предшествующим знаку @. Эти сообщения появляются только в потоке сообщений у пользователя задавшего вопрос или у пользователей читающих данного пользователя. Реализуйте упрощенную версию этого, ограничив появление @replies только в потоках сообщений получателя и отправителя. Это может подразумевать добавление in_reply_to столбца в таблицу microposts и дополнительного including_replies пространства к модели Micropost.
Поскольку нашему приложению не хватает уникальных пользовательских логинов, вам также необходимо решить, каким способом представлять пользователей. Один из вариантов это использование комбинации id и имени, например @1-michael-hartl. Другой способ это добавить уникальное имя пользователя в процесс регистрации и затем использовать его в @replies.

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

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

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

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

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

В настоящее время, если пользователи нашего приложения забудут свои пароли, они не смогут их восстановить. Из-за одностороннего безопасного хэширования паролей в Главе 7, наше приложение не может отправить по email пароли пользователей, но оно может отправить ссылку на форму сброса пароля. Введите PasswordReminders ресурс для реализации этой функции. Вам следует создавать уникальный token для каждого сброса и отправлять его пользователю. Посещение URL с token должно позволять им сбрасывать старый пароль и назначать новый.

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

Помимо регулярного выражения для электронной почты, пример приложения в настоящее время не имеет способа проверки валидности пользовательского 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 блоков (Раздел 12.2.5) ко многим действиям Application контроллера; они должны отвечать на запросы для XML. Позаботьтесь о безопасности; API должен быть доступен только авторизированным пользователям.

Поиск

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

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

Существует огромное количество Rails ресурсов в магазинах и в сети — предложений настолько много что это подавляет и может быть сложно понять, с чего же именно начать. Теперь вы знаете с чего начать — с этой книги, конечно. И если вы дошли до этого места, вы готовы практически ко всему за ее пределами. Вот несколько советов:
  • Ruby on Rails Tutorial screencasts: я подготовил полноценный скринкаст курс основанный на этой книге. В дополнение к раскрытиювсех материалов этой книги, скринкасты дополнены советами, трюками и демонстрациями типа смотри-как-это-делается, которые сложно зафиксировать в печатном варианте. Они доступны на сайте Ruby on Rails Tutorial, через Safari Books Online, и через InformIT.
  • Railscasts: Трудно переоценить важность ресурса Railscasts, я советую начать с посещения архива эпизодов Railscasts и клика по любой зацепившей вас теме.
  • Масштабирование Rails: Одна тема в Ruby on Rails Tutorial осталась почти нетронутой: производительность, оптимизация и масштабирование. К счастью, большинство сайтов никогда не столкнется с серьезными проблемами масштабирования, и использование чего-либо кроме простого Rails для оптимизации вероятно будет преждевременным. Если вы столкнулись с проблемами производительности, Scaling Rails серия от Gregg Pollack Envy Labs является прекрасным местом для старта. Я также рекомендую исследовать приложения для мониторинга сайтов Scout и New Relic.26 И, как вы к этому времени можете подозревать, есть Railscasts по многим темам масштабирования, включая профилирование, кэширование и фоновые процессы.
  • Ruby и Rails книги: Как упоминалось в Главе 1, я рекомендую Beginning Ruby Петера Купера, The Well-Grounded Rubyist David A. Black, и The Ruby Way Хэла Фултона fдля дальнейшего изучения Ruby, и The Rails 3 Way Obie Fernandez для подробной информации о Rails.
  • PeepCode: PeepCode: я упоминал несколько коммерческих скринкастеров в Главе 1,но только с одним из них у меня есть значительный опыт и это PeepCode. Скринкасты на PeepCode неизменно высокого качества и я горячо рекомендую их.

12.5 Упражнения

  1. Добавьте тесты для dependent :destroy в модели Relationship (Листинг 12.5 и Листинг 12.17) следуя примеру в Листинге 11.11.
  2. Метод respond_to виденый в Листинге 12.36 на самом деле может быть поднят из действий в сам контроллер Relationships, и respond_to блоки могут быть заменены на Rails метод respond_with. Подтвердите, что результирующий код, показаный в Листинге 12.47, является корректным проверив что набор тестов все еще проходит. (Подробнее об этом методе см в Google поиске “rails respond_with”.)
  3. Действия following и followers в Листинге 12.29 до сих пор имеют значительное дублирование. Проверьте что show_follow метод в Листинге 12.48 устраняет это дублирование. (Посмотрим, сможете ли вы вывести чем занимается метод send, как в, например, @user.send(:following).)
  4. Реорганизуйте Листинг 12.30 добавив партиалы для кода общего для читаемые/читатели страниц, Home страницы, и страницы показывающей пользователя.
  5. Следуя модели в Листинге 12.20, напишите тесты для статистики на странице профиля.
  6. Напишите интеграционный тест для чтения и не чтения сообщений пользователя.
  7. Перепишите Ajax методы из Раздела 12.2.5 используя jQuery вместо Prototype. Подсказка: вы можете почитать jQuery раздел Mikel Lindsaar’s blog post about jQuery, RSpec, and Rails 3. Также см. Раздел 13.1.4.2, в котором обсуждается jQuery в контексте Rails 3.1.
Листинг 12.47. Компактный рефакторинг Листинга 12.36.
class RelationshipsController < ApplicationController
  before_filter :authenticate

  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
Листинг 12.48. Реорганизованные following и followers действия.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def following
    show_follow(:following)
  end

  def followers
    show_follow(:followers)
  end

  def show_follow(action)
    @title = action.to_s.capitalize
    @user = User.find(params[:id])
    @users = @user.send(action).paginate(:page => params[:page])
    render 'show_follow'
  end
  .
  .
  .
end
  1. Фотографии для макетов взяты на http://www.flickr.com/photos/john_lustig/2518452221/ и http://www.flickr.com/photos/30775272@N05/2884963755/
  2. Для простоты, Рис. 12.6 подавляет id столбец таблицы following
  3. К сожалению, Rails использует connection для подключения к базе данных, поэтому введение Connection модели приведет к некоторым довольно тонким багам. (Я узнал это на собственной шкуре при разработке Insoshi.) 
  4. Действительно, эта конструкция является столь характерной для Rails, что известный Rails программист Josh Susser использует ее в качестве названия для своего гик блога. 
  5. Технически, Rails использует метод underscore для преобразования имени класса в id. Например, "FooBar".underscore это foo_bar, поэтому внешним ключом для объекта FooBar будет foo_bar_id. (Кстати, инверсией underscore является camelize, который конвертирует camel_case в CamelCase.) 
  6. Если вы заметили что followed_id также идентифицирует пользователя, и обеспокоены ассиметричным обращением с читателями и читаемыми, вы готовы к любым неожиданностям. Мы займемся этим вопросом в Разделе 12.1.5
  7. Этот метод follow! должен работать всегда, поэтому, (следуя модели create! и save!) мы ставим восклицательный знак и в случае провала будет вызвано исключение. 
  8. Once you have a lot of experience modeling a particular domain, you can often guess such utility methods in advance, and even when you can’t you’ll often find yourself writing them to make the tests cleaner. In this case, though, it’s OK if you wouldn’t have guessed them. Software development is usually an iterative process—you write code until it starts getting ugly, and then you refactor it—but for brevity the tutorial presentation is streamlined a bit. (??) 
  9. Метод authenticate_with_salt включен только в качестве ориентира в файле модели User. 
  10. Метод unfollow! не вызывает исключения при неудаче — фактически я даже не знаю как Rails указывает на провальное уничтожение — но мы используем восклицательный знак для поддержания follow!/unfollow! симметрии. 
  11. Вы могли заметить, что иногда мы получаем доступ к id явно, как в followed.id, а иногда мы просто используем followed. Мне стыдно признаться, но мой обычный алгоритм для выяснения того, что id можно опустить, заключается в проверке работает ли код без .id, а затем в добавлении .id если не сработало. 
  12. Все что содержится в Листинге 12.28 было раскрыто в других местах этого руководства, так что это хорошее упражнение в чтении кода. 
  13. Так как номинально это является акронимом asynchronous JavaScript and XML, Ajax иногда ошибочно пишут как “AJAX”, хотя на протяжении всей оригинальной Ajax статьи используется написание “Ajax”. 
  14. Это работает, только если JavaScript включен в браузере, но изящно деградирует, работая в точности как в Разделе 12.2.4 если JavaScript отключен. 
  15. В этот момент вы должны включить дефолтную Prototype JavaScript Library в вашем Rails приложение как в Листинге 10.39 если вы этого еще не сделали. 
  16. Нет никакой связи между этим respond_to и respond_to используемым в RSpec примерах. 
  17. Основное требование заключается в том, что перечисляемые объекты должны реализовывать each метод для перебора коллекции. 
  18. Эта запись фактически началась, как расширение Rails, внесенное в ядро ​​языка Ruby; она была настолько полезной, что в настоящее время она включена в сам Ruby. Замечательно, правда? 
  19. Вызов paginate на объект Array преобразует его в объект WillPaginate::Collection, но это не сильно нам поможет, поскольку весь массив уже создан в памяти. 
  20. Функция в комплекте с элементом данных (пользователем, в данном случае) называется замыкание, мы с ним сталкивались при кратком обсуждении блоков в Разделе 4.3.2
  21. Для более продвинутых способов создания необходимых подзапросов, см. сообщение в блоге “Hacking a subselect in ActiveRecord”. 
  22. Конечно, даже подзапрос не масштабирует навсегда. Для больших сайтов вам, вероятно потребуется генерировать поток сообщений асинхронно используя фоновый процессы. Такие тонкости масштабирования выходят за рамки этого учебника, но скринкасты Scaling Rails являются хорошей отправной точкой. 
  23. Для того чтобы сделать поток сообщений более привлекательным в Рис. 12.20, я добавил несколько дополнительных микросообщений вручную используя Rails консоль. 
  24. В этом можно убедиться, изучив SQL выражения в в лог-файле сервера разработки. (Rails Tutorial screencasts раскрывают подобные тонкости более подробно.) 
  25. Единственная моя оговорка по поводу Railscasts — они обычно опускают тесты. Это, вероятно, необходимо для сохранения красоты и краткости эпизодов, но у вас может сформироваться неправильное представление о важности тестов. После просмотра соответствующего Railscast для получения представления о процессе, я советую писать новую фнкцию используя разработку через тестирование. 
  26. In addition to being a clever phrase — new relic being a contradiction in terms (??) — New Relic также является анаграммой имени основателя компании, Lew Cirne. 
  27. # позволю себе небольшой комментарий к переводу этой главы. В тексте очень часто употребляются различные формы слова follow (following, follower, followers и т.п.) в дословном переводе это означает "следовать" (слежение, следящий, следящие соответственно). Однако Twitter, (клоном которого является пример приложения, рассматриваемый в этом учебнике) вместо дословного перевода, использует термины "Читает" (для following) и "Читают" (для followers) и лично мне термин "Читатели" вместо возможного дословного перевода (следователи, последователи и т.п.) нравится больше. И, несмотря на несколько казусов с этим связанных, в оставшейся части учебника я постараюсь придерживаться именно этого варианта.
    P.s. Надеюсь данный комментарий не запутал вас окончательно. :)