Вы находитесь на странице: 1из 465

ДЛЯ ПРОФЕССИОНАЛОВ Г

Гибкая разработка
веб-приложений
в среде Rails

Дэйв Томас
Дэвид Хэнссон

r a ils 4-е издание ПИТЕР


Е^ППТЕР
Agile Web Development with
Rails
Fourth Edition

Sam Ruby
Dave Thomas
David Heinem eier Hansson

w ith Leon Breedt


M ike Clark
James Duncan Davidson
Justin Gehtland
Andreas Schwarz

The Pragmatic Bookshelf___


D a lla s, T e x a s • R a le ig h , N o rth C a ro lin a
Гибкая разработка
веб-приложений
в среде Rails
4-е издание

Сэм Руби
Дэйв Томас
Дэвид Хэнссон

С ^П П ТЕР
М осква ■Санкт-Петербург ■Нижний Новгород ■Воронеж
Ростов-на-Дону ■Екатеринбург ■С ам ара ■Новосибирск
Киев ■Харьков ■М инск
2012
ББК 32.988.02
УДК 004.738.5
Р82

Руби С., Томас Д., Хэнссон Д.


Р82 Гибкая разработка веб-приложений в среде Rails. 4-е изд. — СПб.: Питер,
2 0 12 .— 464 с.: ил.

ISBN 978-5-459-00312-3
П е р е д вам и н о в о е и зд а н и е б е с т с е л л е р а « A g ile w e b d e v e lo p m e n t w ith R a ils » , н а п и с а н н о г о С э м о м
Р у б и — р у к о в о д и т е л е м A p a c h e S o ftw a r e F o u n d a tio n и р а зр а б о т ч и к о м ф о р м а та A to m , Д э й в о м Т о м а ­
с о м — а в т о р о м книги « P r o g ra m m in g R u b y » и Д э в и д о м Х э н с с о н о м — с о з д а т е л е м т е х н о л о г и и R a ils.
R a ils п р ед ст а в л я ет с о б о й с р е д у , о б л е г ч а ю щ у ю р а зр а б о т к у , р а зв ер т ы в а н и е и о б с л у ж и в а н и е в е б ­
п р и л о ж е н и й . З а в р ем я , п р о ш е д ш е е с м о м е н т а е е п е р в о г о р ел и за , R a ils п р о ш л а путь о т м а л о и зв е с т ­
н о й т е х н о л о г и и д о ф е н о м е н а м и р о в о г о м а сш т а б а и ст а л а и м е н н о т о й с р е д о й , к о т о р у ю в ы б и р а ю т ,
ч т обы со зд а в а т ь так н а зы в а ем ы е « п р и л о ж е н и я W eb 2 .0 » .
Э т а книга, у ж е д а в н о став ш ая н а ст о л ь н о й п о и з у ч е н и ю R u b y o n R a ils, п р е д н а зн а ч е н а д л я в сех
п р о гр а м м и ст о в , с о б и р а ю щ и х с я со зд а в а т ь и разв ер ты в ать со в р е м е н н н ы е в е б -п р и л о ж е н и я . И з п е р ­
вой ч асти книги вы п о л у ч и т е н а ч а л ь н о е п р е д с т а в л е н и е о язы к е R u b y и о б щ и е с в е д е н и я о са м о й
с р е д е R a ils. Д а л е е н а п р и м е р е с о зд а н и я и н т е р н е т -м а г а зи н а вы и зу ч и т е к о н ц е п ц и и , п о л о ж е н н ы е
в о с н о в у R a ils. В т р ет ь е й ч аст и р а ссм а т р и в а ет ся вся э к о с и с т е м а R a ils: е е ф у н к ц и и , в о зм о ж н о с т и
и д о п о л н и т е л ь н ы е м о д у л и . Ч ет в ер т о е и зд а н и е книги о п и с ы в а ет р а б о т у с R a ils 3.1 и R u b y 1 .9 .2 .

ББК 32.988.02
УДК 004.738.5

Права на издание получены по соглашению с Pragmatic Bookshelf. Все права защищены. Никакая часть данной
книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев
авторских прав.

Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как


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

ISBN 978-1934356548 англ. ©2011 Pragmatic Programmers, LLC.


ISBN 978-5-459-00312-3 © Перевод на русский язык ООО Издательство «Питер», 2012
© Издание на русском языке, оформление ООО Издательство «Питер», 2012
Оглавление

Предисловие к четвертому изданию .......................................................................10

Предисловие к версии книги для Rails 3 . 1 ............................................................ 11


Благодарности.........................................................................................................12

Введение............................................................................................ 14
Rails является средством гибкой разработки..................................................... 16
Для кого предназначена эта книга........................................................................17
Как нужно читать эту к н и гу .................................................................................18
От издательства.......................................................................................................20

Часть I. Начало.................................................................................. 21
Глава 1. Установка R a ils.............................................................................................22
1.1. Установка под Windows.................................................................................. 23
1.2. Установка под Mac OS X ................................................................................ 24
1.3. Установка под Linux........................................................................................25
1.4. Выбор версии R ails..........................................................................................27
1.5. Настройка среды разработки......................................................................... 27
1.6. Rails и базы данных.......................................................................................... 31
1.7. Наши достижения........................................................................................... 33
Глава 2. Немедленное использование.................................................................... 34
2.1. Создание нового приложения....................................................................... 34
2.2. Привет, Rails!.....................................................................................................36
2.3. Соединение страниц........................................................................................44
2.4. Наши достижения........................................................................................... 46
Глава 3. Архитектура Rails-приложений................................................................48
3.1. Модели, представления и контроллеры...................................................... 48
3.2. Поддержка модели R ails................................................................................ 51
3.3. Action Pack: представление и контроллер...................................................53
Глава 4. Введение в R u b y .......................................................................................... 56
4.1. Ruby — объектно-ориентированный я з ы к .................................................57
4.2. Типы данных.................................................................................................... 59
4.3. Л огика................................................................................................................62
6 Оглавление

4.4. Организационные структуры....................................................................... 65


4.5. Маршализированные объекты ..................................................................... 68
4.6. А теперь все вместе......................................................................................... 69
4.7. Идиомы, используемые в Ruby..................................................................... 70

Часть II. Создание приложения..................................................... 73


Глава 5. Интернет-магазин ......................................................................................74
5.1. Поэтапная разработка....................................................................................74
5.2. Для чего предназначен D ep o t.................................................................... 75
5.3. А теперь приступим к программированию.............................................. 79
Глава 6. Задача А: создание приложения..............................................................81
6.1. Шаг А1: создание приложения по учету товаров................................... 81
6.2. Шаг А2: улучшение внешнего вида перечня товаров............................89
6.3. Наши достижения........................................................................................... 94
Глава 7. Задача Б: проверка приемлемости данных
и блочное тестирование........................................................................................97
7.1. Шаг Б1: проверка приемлемости данных.................................................. 97
7.2. Шаг Б2: блочное тестирование моделей................................................ 102
7.3. Наши достижения.......................................................................................110
Глава 8. Задача В: отображение каталога товаров............................................ 112
8.1. Шаг В1: создание каталога товаров......................................................... 113
8.2. Шаг В2: добавление макета страницы....................................................117
8.3. Шаг ВЗ: использование помощника для форматирования цены . . . . 120
8.4. Шаг В4: функциональное тестирование контроллеров...................... 121
8.5. Наши достижения.......................................................................................124
Глава 9. Задача Г : создание корзины покупателя............................................ 126
9.1. Шаг И : обнаружение корзины.................................................................126
9.2. Шаг Г2: связывание товаров с корзинами.............................................. 127
9.3. Шаг ГЗ: добавление кнопки...................................................................... 130
9.4. Наши достижения.......................................................................................135
Глава 10. Задача Д: усовершенствованная корзина..........................................136
10.1. Шаг Д1: создание усовершенствованной корзины ............................136
10.2. Шаг Д2: обработка ош ибок.................................................................... 141
10.3. Шаг ДЗ: завершение разработки корзины .......................................... 144
10.4. Наши достижения..................................................................................... 148
Глава 11. Задача Е: добавление A J A X .................................................................. 150
11.1.Шаг Е1: перемещение корзи н ы ...............................................................151
Оглавление 7

11.2. Шаг Е2: создание корзины на основе AJAX-технологии.................. 157


11.3. Шаг ЕЗ: выделение изменений...............................................................161
11.4. Шаг Е4: предотвращение отображения пустой корзины.................. 163
11.5. Шаг Е5: придание изображениям восприимчивости к щелчкам ... 167
11.6. Тестирование изменений, внесенных при добавлении AJAX........... 169
11.7. Наши достижения..................................................................................... 172

Глава 12. Задача Ж : оформление покупки....................................................... 174


12.1. Шаг Ж1: регистрация заказа...................................................................174
12.2. Шаг Ж2: применение Atom-канала....................................................... 187
12.3. Шаг ЖЗ: разбиение на страницы........................................................... 190
12.4. Наши достижения.....................................................................................194

Глава 13. Задача 3: отправка электронной п о ч т ы ............................................ 195


13.1. Шаг 31: отправка подтверждающих электронных сообщений....... 195
13.2. Шаг 32: комплексное тестирование приложений............................... 203
13.3. Наши достижения..................................................................................... 208

Глава 14. Задача И: вход в административную о б л а с т ь .................................209


14.1. Шаг И1: добавление пользователей......................................................210
14.2. Шаг И2: аутентификация пользователей............................................ 214
14.3. Шаг ИЗ: ограничение доступа.................................................................220
14.4. Шаг И4: добавление боковой панели и дополнительных
административных ф ункций.................................................................222
14.5. Наши достижения..................................................................................... 226

Глава 15. Задача К: локализация.......................................................................... 228


15.1. Шаг К1: выбор региона............................................................................229
15.2. Шаг К2: перевод каталога товаров....................................................... 232
15.3. Шаг КЗ: перевод оформления заказа....................................................238
15.4. Шаг К4: добавление переключателя локализации..............................244
15.5. Наши достижения..................................................................................... 246
Глава 16. Задача Л: развертывание и эксплуатация....................................... 248
16.1. Шаг Л 1: развертывание с использованием Phusion Passenger
и M ySQL....................................................................................................249
16.2. Шаг Л2: удаленное развертывание с помощью Capistrano...............256
16.3. Шаг ЛЗ: проверка работы развернутого приложения..........................262
16.4. Наши достижения....................................................................................... 265
Глава 17. Ретроспектива D e p o t.............................................................................266
17.1. Концепции Rails...........................................................................................266
17.2. Документирование проделанной работы .............................................. 269
8 Оглавление

Часть III. Углубленное изучение R a ils..................................... 271


Глава 18. Ориентация в мире R a i l s ...................................................................... 272
18.1. Где что размещается................................................................................. 272
18.2. Соглашения об им енах............................................................................281
18.3. Наши достижения..................................................................................... 284
Глава 19. Active R ecord............................................................................................ 285
19.1. Определение структуры ваших данных.................................................. 285
19.2. Определение местоположения записей и прослеживание
их связей......................................................................................................290
19.3. Создание, чтение, обновление, удаление (CRUD — Create, Read,
Update, D elete)...........................................................................................294
19.4. Участие в процессе мониторинга..............................................................311
19.5. Транзакции.................................................................................................. 318
19.6. Наши достижения....................................................................................... 321
Глава 20. Action Dispatch и Action C ontroller.................................................... 323
20.1. Направление запросов контроллерам....................................................324
20.2. Обработка запросов................................................................................... 335
20.3. Объекты и операции, расширяющие диапазон действия запросов 346
20.4. Наши достижения.......................................................................................356
Глава 21. Action V ie w .............................................................................................. 357
21.1. Использование шаблонов..........................................................................357
21.2. Создание форм............................................................................................ 359
21.3. Обработка форм...........................................................................................362
21.4. Выкладывание файлов для Rails-приложений.....................................364
21.5. Использование помощников.................................................................... 367
21.6. Сокращение объемов поддержки приложения с помощью
макетов и парциалов................................................................................. 374
21.7. Наши достижения.......................................................................................382
Глава 22. Кэширование............................................................................................384
22.1. Эффективное использование кэш-памяти на стороне клиента....... 385
22.2. Страничное кэширование..........................................................................389
22.3. Завершение использования страниц......................................................392
22.4. Фрагментарное кэширование.................................................................. 396
22.5. Наши достижения....................................................................................... 401
Глава 23. Миграции.................................................................................................. 402
23.1. Создание и запуск миграций.................................................................... 403
23.2. Внутреннее устройство миграции........................................................... 405
23.3. Управление таблицами..............................................................................409
Оглавление 9

23.4. Расширенное применение м играций......................................................414


23.5. Слабая сторона миграций..........................................................................417
23.6. Манипуляции со схемой данных вне миграций................................... 418
23.7. Наши достижения.......................................................................................419
Глава 24. Приложения, не использующие бр аузер ......................................... 420
24.1. Автономное приложение, использующее Active R ecord.....................421
24.2. Библиотечная функция, использующая Active S u p p o rt.................... 422
24.3. Удаленное обращение с использованием Active Resource.................. 426
24.4. Наши достижения.......................................................................................433
Глава 25. Зависимости R ails................................................................................... 434
25.1. Генерирование XML с помощью Builder................................................ 435
25.2. Генерирование HTML с помощью E R b ..................................................436
25.3. Управление зависимостями с помощью B undler................................. 438
25.4. Взаимодействие с веб-сервером с помощью R ack ................................441
25.5. Автоматизация задач с помощью R a k e ..................................................444
25.6. Обзор Rails-зависимостей........................................................................445
25.7. Наши достижения.......................................................................................449
Глава 26. Дополнительные модули R ails.............................................................450
26.1. Обработка кредитных карт с помощью Active M erchant....................450
26.2. Украшение нашей разметки с помощью Haml.......................................452
26.3. Поиск дополнительных модулей на сайте RailsPlugins.org...............455
26.4. Наши достижения...................................................................................... 456
Глава 27. Куда двигаться дальше..........................................................................457

Алфавитный указатель.................................................................459
Предисловие к четвертому изданию

Когда Дэйв попросил меня присоединиться к этому проекту в качестве соавтора


третьего издания книги, я испытал глубокое волнение. В конце концов, ведь я
изучал Rails по первому изданию этой книги. Кроме того, у меня с Дэйвом много
общего. Хотя он предпочитает Emacs и Mac OS X, а я люблю Vim и Ubuntu, оба
мы отдаем предпочтение командной строке, и наши пальцы привыкли набивать
код, а прежде чем углубляться в сплошную теорию, мы начинаем с реальных при­
меров.
Со времени публикации третьего издания (фактически со времени выхода
первого, второго и третьего изданий) многое успело измениться. Rails находится
в процессе глубокой переработки, затрагивающей внутреннее устройство. Многие
средства, применявшиеся в предыдущих примерах, сначала попали в разряд не
рекомендуемых, а затем и вовсе были удалены. Были добавлены новые средства, и,
кроме того, был накоплен огромный опыт наилучшего использования Rails. Теперь
Rails работает с Ruby 1.9, и каждый пример был протестирован как с Ruby 1.8.7,
так и с Ruby 1.9.2.
Rails выросла из популярной среды в активную и полную жизни экосистему,
укомплектованную популярными дополнительными модулями и глубоко инте­
грированную в сторонние инструментальные средства. В процессе своего развития
среда Rails превратилась в общепринятое средство, привлекающее все более раз­
нообразный контингент разработчиков.
Все это заставило изменить структуру книги. Многие новички в Rails не знали
о языке Ruby, поэтому данный раздел переместился из приложения в первую часть
книги, став отдельной главой. Затем настал черед разработки приложения, которое
было обновлено и упрощено с прицелом на текущий позитивный опыт.
Но наибольшим изменениям подверглась заключительная часть. Поскольку
сейчас нет смысла обсуждать всю систему Rails, огромную и постоянно меняющую­
ся, эта часть сконцентрирована на общей концепции всей структуры, позволяющей
читателю понять, что нужно искать и где можно найти дополнительные модули и
сопутствующие инструментальные средства, отвечающие запросам, выходящим
далеко за рамки содержимого самой среды.
Короче говоря, эта книга нуждалась в очередной переделке.
Сэм Руби (Sam Ruby)
Предисловие к версии книги
для Rails 3.1

Эта книга о Rails 3.1.


Даже с учетом того, что, согласно заявлению, Rails 3.1 является промежуточным
выпуском и включает в себя минимальное количество более-менее существенных
изменений (конечно, по сравнению с изменениями, которые произошли между
Rails 2.3.x и Rails 3.0), этот выпуск содержит значительное количество новшеств, с
которыми столкнутся пользователи.
Рассмотрим краткий обзор некоторых наиболее существенных изменений:
О Сгенерированный код «знает», какую версию Ruby вы используете. В ре­
зультате этого появляются преимущества от использования нового, более
простого синтаксиса значений хэша, который стал доступен в Rubv 1.9.2. что
влечет за собой изменение формата вывода тестовых данных.
О Управление ресурсами теперь стало частью ядра Rails. Это изменило место­
нахождение ряда файлов и привело к добавлению еще одного шага к раз­
вертыванию приложения.
О По умолчанию стало доступно использование SCSS. что изменило как син­
таксис, так и организацию всех таблиц стилей.
О Библиотека JQ uery заменила Prototype и Script.aculo.us, что повлекло за
собой изменения в коде JavaScript, который вы будете создавать в главе 11
«Задача Е: добавление AJAX».
О По умолчанию стала доступна библиотека CoffeeScript, изменившая синтак­
сис сценариев, выполняемых на стороне клиента.
О Методы миграций перестали быть методами класса и в большинстве своем
стали автоматически реверсируемыми.
О Теперь по умолчанию для преобразования ответов в последовательную фор­
му используется не XML, a JSON. Также появился новый вспомогательный
метод представления по имени j , облегчающий создание JSO N -ответов
(и, в целом, JavaScript).
О К модели добавился метод h a s_ se cu re _ p a ssw o rd , инкапсулирующий и стан­
дартизирующий логику хэширования обычного пользовательского пароля.
12 Предисловие к версии книги для Rails 3.1

О В рабочем режиме по умолчанию стал доступен модуль R a c k : : Cache.


О Сет-пакет m y s q l был заменен gem-пакетом m ysql2.
Дополнительные подробности можно найти в примечании к выпуску1.
Для запуска примеров, представленных в данной книге, важно установить
правильную версию Rails в соответствии с описаниями, которые даны в главе 1
«Установка Rails». Если вы примете решение загрузить примеры в соответствии
с описанием, которое дано в разделе введения «Как читать эту книгу», убедитесь
в том, что вы выбрали файлы из каталога rails31.
Для определения номера запущенной версии Rails можно выполнить в команд­
ной строке команду n a i l s -v.
Дополнительная информация, касающаяся изменений, внесенных в Rails, кото­
рые влияют на материал данной книги, может быть найдена по адресу http://www.
pragprog.com/wikis/wiki/ChangesToRails.

Благодарности
Вы, наверное, думаете, что на выпуск нового издания книги нужно потратить мень­
ше усилий. Ведь, в конце концов, весь текст уже есть. Остается только подправить
кое-где код, внести в некоторые места незначительные правки — и дело в шляпе.
Да не тут-то было...
Трудно передать все в точности, но у нас сложилось впечатление, что создание
каждого издания этой книги требовало от нас чуть ли не больше усилий, чем вы­
пуск самого первого издания. Rails постоянно развивается, и вместе с ней изменя­
ется и данная книга. Части приложения Depot переписывались по нескольку раз,
кроме этого обновлялись все комментарии к этому коду. Акцент на применении
REST и исключение ряда средств по мере того, как они попадали в разряд не реко­
мендуемых, неоднократно приводили к изменению структуры книги, как только
горячее становилось еле теплым.
Поэтому данная книга не появилась бы на свет без огромной помощи со сторо­
ны сообществ Ruby и Rails. Для начала перечислю очень полезных официальных
рецензентов проектов данной книги:
Джереми Андерсон (Jeremy Anderson), Кен Коар (Кеп Соаг), Джеф Коэн (Jeff
Cohen), Джоел Клермонт (Joel Clermont), Джеф Дрейк (Geoff Drake), Паван
Горакави (Pavan Gorakavi), Майкл Юревич (Michael Jurewitz), Майкл Линд-
саар (Mikel Lindsaar), Пол Рейнер (Paul Rayner), Мартин Реверс (M artin
Reuvers), Даг Ротен (Doug Rhoten), Гари Шерман (Gary Sherman), Даванум
Шринивас (Davanum Srinivas), Стефан Туралски (Stefan Turalski) и Хосе
Валим (Jose Valim).
Кроме этого, каждое издание данной книги выпускалось в бета-версии: каждая
версия публиковалась в PD F-формате, и люди комментировали ее в Интернете.

1 http://guides.rubyonrails.org/3_l_release_notes.html
Благодарности 13

В результате оставленных ими комментариев только для этого издания поступило


800 предложений и сообщений об ошибках.
Значительная часть предложений в конечном итоге была принята, что сделало
эту книгу намного полезнее. Хотя наша благодарность распространяется на всех,
кто поддерживал программу выпуска бета-версии книги и внес в своих отзывах
так много ценных предложений, среди них есть люди, которые при этом вносили
свой вклад не по долгу службы:
Мануэль Э. Видаурре Аренас (Manuel Е. Vidaurre Arenas), Сет Арнолд (Seth
Arnold), Уилл Боулин (Will Bowlin),Энди Брайс (Andy Brice), Джейсон Ка-
тена (Jason Catena), Виктор Мариус Костан (Victor Marius Costan), Дэвид
Хэдли (David Hadley), Джейсон Холловей (Jason Holloway), Дэвид Kanri
(David Карр), Trung LE, Кристиан Рибер Мандруп (Kristian Riiber Mandrup),
mltsy, Стив Николсон (Steve Nicholson), Джим Пале (Jim Puls), Джонатан
Ритци (Johnathan Ritzi), Leonel S, Ким Шриер (Kim Shrier), Дон Смит (Don
Smith), Джо Страйтиф (Joe Straitiff) и Мартин Золлер (M artin Zoller).
И наконец, огромная помощь была оказана со стороны команды разработчиков
ядра Rails, представители которой отвечали на вопросы, проверяли наши фраг­
менты кода и исправляли ошибки. Мы говорим огромное спасибо следующим
специалистам:
Скотт Баррон (Scott Barron, htonl), Джеймис Бак (Jamis Buck, minam),
Томас Фатч (Thomas Fuchs, madrobby), Джереми Кемпер (Jeremy Kemper,
bitsweat), Иегуда Кац (Yehuda Katz, wycats), Майкл Козарски (Michael Ko-
ziarski, nzkoz), Марсель Молина мл. (Marcel Molina Jr, noradio), Рик Олсон
(Rick Olson, technoweenie), Николас Секар (Nicholas Seckar, Ulysses), Сэм
Стефенсон (Sam Stephenson, sam), Тобиас Лютке (Tobias Lutke, xal), Xoce
Валим (Jose Valim, josevalim) и Флориан Вебер (Florian Weber, csshsh).

Сэм Руби (Sam Ruby)


mailto:rubys@intertwingly.net
Введение

Ruby on Rails является средой, облегчающей разработку, развертывание и обслу­


живание веб-приложений. За время, прошедшее с ее начального выпуска, Rails
прошла путь от малоизвестной технологии до феномена мирового масштаба и, что
более важно, стала именно той средой, которую выбирают, чтобы создавать так на­
зываемые приложения Web 2.0.
Почему это произошло?
Просто Rails хорошо прижилась с самого начала. Большое количество разработ­
чиков было недовольно теми технологиями, которые применялись ими для созда­
ния веб-приложений. И дело, наверное, не в том, что именно они использовали —
Java, РН Р или .NET, — у них накапливалось ощущение излишней трудоемкости
их работы. А затем в один прекрасный момент пришла Rails, с которой работать
стало намного проще.
Но сама по себе простота не означает упрощенность. Речь идет о профес­
сиональных разработчиках, создающих по-настоящему востребованные во всем
мире веб-сайты. Им хочется видеть созданные ими приложения выдержавшими
испытание временем —спроектированными и разработанными с использованием
современных, профессиональных технологий. Поэтому разработчики занялись
Rails всерьез и обнаружили, что она является не только инструментом для раз­
работки веб-сайтов.
К примеру, все Rails-приложения выполняются с использованием архитектуры
Модель-Представление-Контроллер ( Model-View-Controller, MVC). Привыч­
ная Java-разработчикам среда выполнения, к примеру Tapestry или Struts, тоже
основана на MVC. Но Rails идет в использовании MVC еще дальше: при ведении
разработки в Rails вы начинаете уже с работающего приложения, в котором есть
место для каждой части кода, и все части вашего приложения стандартным образом
взаимодействуют друг с другом.
Профессиональные программисты пишут тесты. И Rails опять вносит свою
лепту. Все Rails-приложения имеют встроенное тестирование. По мере добавления
к программному коду какой-либо функциональной возможности, Rails автоматиче­
ски создает программные заглушки тестов, предназначенные для ее тестирования.
Эта среда облегчает тестирование своих приложений, подстегивая тем самым раз­
работчиков к этому занятию.
Введение 15

Rails-приложения пишутся на Ruby —современном объектно-ориентированном


языке сценариев. Лаконичность кода Ruby не влияет на его разборчивость —свои
идеи на этом языке можно выражать вполне четко и естественно. В результате
чего программы легко пишутся и (что не менее важно) по прошествии нескольких
месяцев вполне легко читаются.
Rails использует все возможности Ruby, являясь его оригинальным расши­
рением, облегчающим жизнь программистов. Программы становятся короче,
читаются легче. Это также позволяет нам выполнять те задачи, которые иначе
выполнялись бы в исходном коде внешних файлов конфигурации. Это облегча­
ет понимание происходящего. Следующий код определяет для проекта модель
класса. Сейчас не стоит вдаваться в детали этого кода —лучше просто подумать
о том, как много информации было выражено в каких-то нескольких строках
программы.
c la s s P r o je c t < A c tiv e R e c o rd : : Base
belo ng s_to : p o r t f o lio
has_one : p roject_m anager
has_many :m ilesto n es
has_many :d e liv e r a b le s , through: m ilesto n es

v a lid a t e s :name, :d e s c r ip t io n , presence: tr u e


v a lid a t e s : n o n _d isclo su re_ag reem en t, accep tance: tr u e
v a lid a t e s : short_nam eJ uniqueness: tru e

Укоротить код Rails и сделать его более читаемым позволяют две другие фило­
софские основы этой среды: DRY и превалирование соглашения над конфигу­
рацией. DRY означает «don’t repeat yourself», то есть «никогда не повторяться»'.
каждая частичка знаний в системе должна быть выражена только в одном месте.
Чтобы воплотить все это в жизнь, Rails пользуется всей эффективностью языка
Ruby. В Rails-приложениях можно увидеть лишь малую долю повторений, то, что
нужно сказать, говорится только в одном месте, которое зачастую предлагается со­
глашениями о MVC-архитектуре, и далее об этом можно уже не беспокоиться. Для
программистов, привыкших работать в других средах веб-разработки, где простое
изменение может заставить их вносить в код программы полдюжины, а то и больше
правок, это было открытием.
Превалирование соглашения над конфигурацией является не менее важным
принципом. Он означает, что в Rails практически для каждого аспекта, связы­
вающего в единое целое ваше приложение, имеются рациональные умолчания.
Следуйте соглашениям, и тогда вы сможете написать Rails-приложение, ис­
пользуя меньше кода, чем в обычном веб-приложении, написанном на Java и ис­
пользующем XML-конфигурацию. Если нужно переписать соглашения, Rails
облегчает и эту задачу.
Разработчики, которые переходят на Rails, замечают еще одну особенность. Rails
не играет в догонялки со ставшими де-факто новыми стандартами: напротив, она
помогает их определять. К тому же Rails облегчает разработчикам интегрирование
в их код таких функций, как интерфейсы AJAX и RESTful, поскольку их поддержка
16 Введение

уже встроена в Rails. (Если вы не знакомы с интерфейсами AJAX и REST, не стоит


беспокоиться, чуть позже мы объясним вам, что это такое.)
Разработчики озабочены также развертыванием своих продуктов. И тут оказы­
вается, что с Rails можно распространять удачную версию своего приложения на
любое количество серверов всего лишь одной командой (и так же легко возвращать
все назад, если версия окажется не вполне удачной).
Rails была выделена из реального коммерческого приложения. Оказалось, что
лучшим способом создания среды является определение основных составляющих
конкретного приложения, а затем занесение их в общий фонд кода. При разработке
Rails-приложения в вашем распоряжении с самого начала уже имеется половина
по-настоящему хорошего приложения.
Но у Rails еще есть кое-что такое, что трудно поддается описанию. Она каким-
то непостижимым образом создает уверенность в правильном выборе. Разумеется,
пока вы самостоятельно не напишете на Rails какие-нибудь приложения (что может
произойти в ближайшие 45 минут), вам придется поверить нам на слово. Обо всем
этом и будет рассказано в нашей книге.

Rails является средством


гибкой разработки
Эта книга называется «Гибкая разработка веб-приложений в среде Rails». Возмож­
но, вы удивитесь, обнаружив отсутствие четко обозначенных разделов, посвящен­
ных использованию гибких методов Rails-программирования.
Объясняется это простыми и в то же время довольно тонкими обстоятельства­
ми. Дело в том, что гибкость разработки является качественной составляющей
Rails.
Давайте взглянем на ценные положения, которые изложены в Манифесте по
гибкой разработке (Agile Manifesto), как на набор из четырех предпочтений1:
О личностей и их взаимодействия над процессами и инструментами;
О работающего программного обеспечения над подробной документацией;
О сотрудничества с заказчиком над контрактными обязательствами;
О реакций на изменения над следованием плану.
В Rails предпочтение целиком отдается людям и их взаимодействию. В ней
отсутствует громоздкий инструментарий, нет ни сложных конфигураций, ни
детально проработанных процессов. Вполне достаточно небольших групп раз­
работчиков с привычными текстовыми редакторами и фрагментами Ruby-кода.
Этот подход ведет к такой прозрачности, при которой все, что делается раз­
работчиками, немедленно отражается на том, что видит заказчик. По сути, это
интерактивный процесс.

1 http://agilemanifesto.org/. Д эй в Томас был одн им из 17 авторов этого документа.


Для кого предназначена эта книга 17

Rails не отвергает документацию. Создание в этой среде HTML-документации


для всего программного кода сводится к весьма простой задаче. Но процесс
разработки в Rails не регулируется какими-либо документами. Вы не найдете в
основе Rails-проекта 500-страничной спецификации. Вместо нее будет группа
пользователей и разработчиков, совместно изучающих свои потребности и воз­
можные пути их удовлетворения. Вы найдете решения, изменяющиеся по мере
того, как и разработчики, и пользователи набираются опыта, пытаясь решить
возникающие проблемы. Вы найдете среду, предоставляющую работоспособное
программное обеспечение на ранней стадии цикла разработки. Это программное
обеспечение может быть грубоватым, но зато оно даст пользователям возмож­
ность составить начальное представление о том, что они от вас получат. Таким
образом, Rails поощряет сотрудничество с заказчиком. Когда заказчики видят,
насколько быстро Rails-проект может реагировать на изменения, они начинают
верить, что команда способна поставить им то, что требуется, а не только то, что
было изначально запрошено. Конфронтации подменяются разговорами на тему:
«А что, если...?».
Все это привязано к идее возможности реагировать на изменения. Одержимость,
с которой в Rails придерживаются принципов DRY, означает, что изменения в
Rails-приложениях оказывают намного меньше влияния на программный код, чем
в других средах. А так как Rails-приложения пишутся на Ruby, где понятия могут
выражаться четко и кратко, изменения чаще всего локализованы и легко вносятся
в программный код. Существенный упор как на блочное, так и на функциональ­
ное тестирование, наряду с поддержкой в процессе тестирования испытательных
стендов и программных заглушек, предоставляет разработчикам страховку, столь
необходимую при внесении изменений. При хорошем наборе тестов изменения
меньше действуют на нервы.
Вместо постоянных попыток связать процессы, происходящие в Rails, с прин­
ципами гибкой разработки, мы решили позволить этой среде самой раскрыть свои
возможности. По мере чтения глав руководства попытайтесь представить себе,
что вы ведете разработку веб-приложения, работая рядом со своими заказчиками
и совместно вырабатывая приоритеты и решения проблем. Затем, при переходе в
части третьей к изучению самых передовых концепций, посмотрите, как структу­
ра, положенная в основу Rails, может позволить вам быстрее и с меньшей долей
формализма удовлетворять потребности ваших заказчиков.
И еще одно, последнее замечание по поводу гибкой разработки и Rails: хотя упо­
минание об этом может выглядеть непрофессионально, но подумайте, насколько
веселее станет сам процесс программирования!

Для кого предназначена эта книга


Эта книга предназначена для программистов, присматривающихся к созданию и
развертыванию веб-приложений. К их числу относятся прикладные программисты,
не работавшие ранее с Rails (и, возможно, даже незнакомые с Ruby), и програм­
18 Введение

мисты, знакомые с основами, но желающие прийти к более глубокому пониманию


среды Rails.
Предполагаются некоторые познания в HTML, Cascading Style Sheets (CSS)
и JavaScript, иными словами, речь идет о способности разбираться в исходном коде
веб-страниц. Вам не нужно быть экспертами по данной тематике, самое сложное,
что предстоит делать, —это переносить в файлы программный материал этой кни­
ги, весь объем которого можно загрузить.

Как нужно читать эту книгу


Первая часть этой книги является подготовительной. После ее прочтения вы по­
лучите начальное представление о языке Rubv и общее представление о самой среде
Rails, у вас будут установлены Ruby и Rails, и вы проверите работоспособность
этой установки на простом примере. В следующей части на более объемном при­
мере (создание простого интернет-магазина) вы изучите концепции, положенные
в основу Rails. Здесь не будет последовательного путешествия по каждому из
компонентов Rails («вот глава о моделях, а вот о представлениях» и т. д.). Эти ком­
поненты созданы для совместной работы, и каждая глава в этой части привязана
к конкретному набору родственных задач, в решение которых вовлекается сразу
несколько совместно работающих компонентов.
Многим, похоже, нравится создавать приложение по мере чтения этой книги.
Если вам не хочется набирать код, можно схитрить, загрузив исходный код (сжатый
tar-архив или zip-файл1). Эта загрузка содержит отдельные наборы исходного кода
для Rails 3.0 и Rails 3.1. Нужные вам файлы будут находиться в каталоге rails31.
Подробности изложены в файле README-FIRST.
В части третьей, «Углубленное изучение Rails», рассматривается вся экосистема
Rails. Эта часть начинается с функций и возможностей Rails, с которыми вы уже
ознакомились. Затем охватывается ряд ключевых зависимостей, используемых
средой Rails, которые вносят непосредственный вклад в общую функциональность,
предоставляемую этой средой. И наконец, в ней рассматривается ряд популярных
дополнительных модулей, расширяющих среду Rails и превращающих ее из про­
стой среды в открытую экосистему.
При чтении книги вам будут попадаться различные принятые нами способы
оформления текста.

ИСПОЛНЯЕМ Ы Й К О Д ------------------------------------------------------------------------------
Больш инство демонстрируемых ф рагментов кода взяты из полноценных, работоспособных
примеров, которые можно загрузить.
Если листинг кода можно найти в загружаемых примерах, чтобы облегчить задачу его поис­
ка, перед ним будет стоять заголовок (такой же, как в следую щем примере).

1 Скачайте файлы с сайта издательства «Питер» w w w .p ite r.co m или по адресу h ttp ://
p rag p ro g .c o m /title s/ra ils4 /so u rce _ c o d e
Как нужно читать эту книгу 19

ra ils 3 1 / w o rk / d e m o l/ a p p / c o n tro lle rs / s a y _ c o n tro lle r, rb


c la s s S a y C o n tro lle r < A p p lic a tio n C o n t r o lle r
► def h e llo
end

► d ef goodbye
end
end

В этом заголовке содержится путь к листингу в скачанной структуре исходно­


го кода. А в некоторых случаях, где используются модификации существующих
файлов и где трудно заметить измененные строки, слева от таких строк можно за­
метить значки в виде треугольников, помогающие их распознать. Две такие строки
показаны в предыдущем примере кода.

Д ЭВИД Г О В О Р И Т ...---------------------------------------------------------------------------------
Время от времени вам будут попадаться врезки « Д э в и д г о в о р и т ...» . В них Дэвид Хай-
немайер Хэнсон (David Heinem eier Hansson) будет делиться с вами ценными сведениями о
Rails — давать пояснения и рекомендации, показывать трюки и т. п. Поскольку он создатель
Rails, эти врезки не стоит пропускать, если вы хотите подойти к изучению этой среды про­
фессионально.

ДЖ О С П РА Ш И В А ЕТ...------------------------------------------------------------------------------
В книге иногда появляется некий мифический разработчик по имени Джо, задаю щий во­
просы по сущ еству изучаемого материала, на которые мы отвечаем.

Эта книга не является справочным руководством по Rails. Наш опыт подска­


зывает, что справочные руководства для большинства людей учебниками служить
не могут. Вместо этого мы показали большинство модулей и многие из их методов
либо в примерах, либо в текстовых описаниях в контексте использования этих
компонентов и их совместной работы.
Вы не найдете здесь и многих сотен страниц с листингами API-функций. Для
этого есть серьезные основания — вы получаете всю соответствующую докумен­
тацию при каждой установке Rails, и она, несомненно, будет новее того материала,
который представлен в данной книге. Если вы устанавливаете Rails, используя
RubyGems (что соответствует нашим рекомендациям), нужно просто запустить
сервер gem-документации (воспользовавшись командой g em _server), и вы сможе­
те получить доступ к описанию всех API-функций Rails, набрав в адресной строке
браузера http://localhost:8808. Создание дополнительной документации и руководств
будет рассмотрено в главе 18, в разделе «Место для документации».
Кроме этого, вы увидите, что сама Rails помогает вам, создавая ответы, четко
идентифицирующие любую найденную ошибку, а также показывая пути, сообщаю­
щие вам не только о месте обнаружения ошибки, но и о том, как до него добраться.
Пример можно увидеть на рис. 10.3. Если нужна дополнительная информация,
загляните в раздел 10.2 «Шаг Д2: обработка ошибок», чтобы увидеть как можно
вставить инструкции для ведения регистрационного журнала.
20 Введение

Если вы на чем-то застопоритесь, помощь можно будет найти в огромной массе


интернет-ресурсов. Вдобавок к упомянутым ранее листингам, есть еще форум1, где
вы можете задавать вопросы и делиться опытом, перечень замеченных опечаток2,
где вы можете сообщить об ошибках, и wiki3, где вы можете обсудить упражнения,
которые приводятся по всей книге.
Это общедоступные ресурсы, и вы можете вполне свободно помещать на форуме
и wiki не только вопросы и проблемы, но также и любые предложения, и ответы на
вопросы, заданные другими людьми.
Итак, приступим! Первым нашим шагом будет установка Ruby и Rails и про­
верка работоспособности этой установки на простом примере.

От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу электронной почты
comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
Все исходные тексты, приведенные в книге, вы можете найти по адресу http://
www.piter.com.
На веб-сайте издательства http://www.piter.com вы найдете подробную информа­
цию о наших книгах.

1 h ttp ://fo ru m s.p ra g p ro g .co m /fo ru m s/1 4 8


2 h ttp ://w w w .p ra g p ro g .co m /title s/ra iis4 /e rra ta
3 h ttp ://w w w .p ra g p ro g .c o m /w ikis/w ik i/R a ilsP la y T im e
Начало
Установка Rails

О сн о в н ы е тем ы :

> установка Ruby, RubyGem s, SQ Lite3 и Rails;


> установка среды разработки и инструм ентария.

В первой части этой книги вам будут представлены язык Ruby и среда Rails. Но сна­
чала нужно установить оба этих средства и убедиться в их работоспособности.
Чтобы среда Rails заработала на вашей системе, нужно располагать следующими
программными средствами:
О интерпретатором Ruby. Система Rails написана на Ruby, и свои приложения вы
также будете создавать на Ruby. Rails 3.1 требует применения Ruby версии 1.9.2
или Ruby 1.8.7. На Ruby 1.8.6 и Ruby 1.9.1 эта среда работать не будет.
Те различия в версиях Ruby, которые касаются материала данной книги,
рассмотрены в ближайшей врезке;
О используемой в Ruby системой упаковки программных средств, а именно
RubyGems. Этот выпуск основан на RubyGems версии 1.8.7;
О системой Ruby on Rails. Эта книга была написана с использованием Rails
версии 3.1 (на данный момент это Rails 3.1,0);
О интерпретатором JavaScript. Встроенные интерпретаторы этого языка име­
ются как в Microsoft Windows, так и в Mac OS X, и Rails будет использовать
ту версию, которая уже установлена на вашей системе. В других операци­
онных системах может понадобиться отдельная установка интерпретатора
JavaScript;
О некоторыми библиотеками, в зависимости от операционной системы;
О базой данных. В данной книге используются как SQLite 3, гак и MySQL 5.1.
Для машины, используемой в целях разработки программ, это практически все,
что нужно (не считая редактора, но о редакторах мы поговорим отдельно). Если
Глава 1 • Установка Rails 23

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


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

ВЫБОР ВЕРСИИ R U B Y -----------------------------------------------------------------------------


М атериал этой книги рассчитан на использование Ruby 1.9.2. Хотя Rails 3.1 работает с Ruby
1.8.7 или с Ruby 1.9.2, команда разработчиков ядра Rails рекомендует для всех новых Rails-
приложений использовать Ruby 1.9.2. У этой версии более соверш енный синтаксис и повы ­
шенная производительность. Более того, ожидается, что в Rails 4.0 поддержка Ruby 1.8.7
будет прекращена.
Если вы все же реш ите установить или оставить на компью тере уж е установленную версию
Ruby 1.8.7, некоторые генерируемые временные платформы (scaffold) будут использовать
«старый» хэш -синтаксис. «Новый» хэш -синтаксис имеет следую щий вид:

r e d in e c t_ to @ cart, n o tic e : 'Корзина успешно со зд ан а.'

А вот как выглядит тот код, который будет сгенерирован Rails 3.1 вместо него при исполь­
зовании Ruby 1.8.7:

r e d ir e c t _ t o @cart_, : n o tic e => ' Корзина успешно созд ан а.'

О братите внимание на изменение позиции символа двоеточия и на дополнительную стрел­


ку, составленную из знаков равенства и «больш е чем».

Также следует учесгь и изменения формата вывода тестов.


Если постоянно помнить об этих двух факторах, то книгу мож но использовать для изучения
Rails 3.1 с использованием Ruby 1.8.7.

1.1. Установка под Windows


Самым простым способом установки Rails под Windows является использование
пакета Railslnstaller1. Нужно лишь убедиться, что вы имеете дело с версией 2.0,
включающей в себя Ruby 1.9.2 и Rails 3.1. На время написания данной книги
Railslnstaller 2.0 был доступен только в предварительном выпуске, ссылка на ко­
торый приведена в сноске. Пока не выйдет окончательная версия пакета, можно
спокойно пользоваться его предварительной версией.
Основная установка происходит довольно просто. 11осле загрузки щелкните на
кнопке Run, затем на кнопке Next. Установите переключатель в положение «I accept
the License» (разумеется, после внимательного прочтения самой лицензии, условия
которой принимаются) и щелкните на кнопках Next ►Install ►Finish.
После этого будет открыто окно командной строки и выдано приглашение на
ввод имени и адреса электронной почты. Это нужно только лишь для настройки
версии системы управления Git. Генерируемый ssh-ключ для выполнения упраж­
нений данной книги не понадобится.

http://ra ilsin sta lle r.org/


24 Часть I • Начало

Закройте это окно и откройте новое окно командной строки, щелкнув на кнопке
Пуск (Start) и выбрав пункт Выполнить... (Run...), после чего введите cmd и щелкните
на кнопке ОК.
А теперь пользователи Windows могут перейти сразу к разделу 1.4, «Выбор
версии Rails». Там мы и встретимся.

1.2. Установка под Mac OS X


Поскольку ни Snow Leopard, ни Lion не имеют предустановленного Ruby 1.9.2, при­
дется загружать и встраивать его самостоятельно. По нашему мнению, проще всего
это сделать с помощью RVM. Установка RVM рассмотрена на веб-сайте RVM1.
В общих чертах эти действия рассматриваются и в данной главе.
Во-первых, нужно убедиться в установке Xcode 3 или более поздней версии. При
работе под управлением Mac OS X 10.7 (Lion) в качестве бесплатного дополнения
можно загрузить Xcode 4.12 из Mac Арр Store, и это средство будет автоматически
установлено на ваш компьютер Мае. При работе под управлением Mac OS X 10.6
(Snow Leopard) нужно установить Xcode с компакт-диска Snow Leopard, поставляе­
мого с компьютером Mac. Xcode можно найти в каталоге Optional Installs. Проверьте
установку, запустив следующую команду:
$ xcodebuild -versio n

Если установлен инструментарий Xcode версии 3, отдельно необходимо будет


установить нужную версию системы управления Git. Загрузите3 и установите ту
версию, которая соответствует версии вашей операционной системы и оборудова­
нию. Проверьте установку, запустив следующую команду:
$ g i t --versio n

Затем установите сам диспетчер RVM:


$ bash < < (c u rl -s h t t p s : //rvm .b egin rescu eend . c o m / in s ta ll/ rv m )

Следуйте инструкциям по установке, выдаваемым данной командой. В част­


ности, нужно добавить установочные инструкции RVM-функции в ваш файл
.bash_profile:

$ echo ' [ [ -s "$Н0МЕ/. rv m /scrip ts/rvm " ] ] && \


. ” $НОМЕ/. rv m / s c rip ts / rv m "' >> ~ / . bash_profile

Выйдите из окна командной строки или приложения Terminal и откройте их


новый экземпляр. Это приведет к перезагрузке с использованием вашего файла
.bash_profile. В этом новом окне установите сам интерпретатор Ruby:
$ rvm i n s t a l l 1 .9 .2

1 h ttp s://rv m .b e g in re sc u e e n d .c o m /rv m /in sta ll/


2 h ttp ://itu n e s.a p p le .c o m /u s/a p p /x c o d e /id 4 4 8 4 5 7 0 9 0 ?m t= 12
3 h ttp ://co d e .g o o g le .co m /p /g it-o sx -in sta lle r/d o w n lo a d s/list?c a n = 3
Глава 1 • Установка Rails 25

Предыдущее действие потребует времени на загрузку, конфигурацию и ком­


пиляцию необходимых исполняемых файлов. Как только работа будет завершена,
воспользуйтесь этой средой окружения и установите Rails:
$ rvm use 1 .9 .2
$ gem i n s t a l l r a i l s

За исключением инструкции rvm u s e , каждая из приведенных выше ин­


струкций должна быть выполнена только один раз. Инструкцию rvm use нужно
повторять при каждом открытии окна оболочки. Ключевое слово u s e является
необязательным, поэтому инструкцию можно сократить до вида rvm 1 . 9 . 2 .
Можно также для новых сеансов работы с терминалом сделать эту версию ин­
терпретатора R u b y используемой по умолчанию, воспользовавшись следующей
командой:
$ rvm - - d e fa u lt 1 .9 .2

Убедиться в успехе установки можно с помощью следующей команды:


$ r a ils -v

Если возникнут проблемы, воспользуйтесь рекомендациями, перечисленными


на веб-сайте rvm под заголовком «Troubleshooting Your Install»1.
А теперь пользователи OS X могут перейти сразу к разделу 1.4, «Выбор версии
Rails», чтобы присоединиться к пользователям Windows. Там мы и встретимся.

1.3. Установка под Linux


Начните с одной из систем управления пакетами, принадлежащей вашей платфор­
ме: apt-get, dpkg, portage, rpm, rug, synaptic, up2date или yum.
Сначала нужно установить необходимые взаимодействующие средства. Сле­
дующие инструкции предназначены для Ubuntu 11.04, Natty Narwhal; при работе
под другими операционными системами и отсутствии уверенности в имеющихся
в них установках самыми важными устанавливаемыми на данном этапе пакетами
являются git и curl, а все остальные пакеты можно будет установить по мере не­
обходимости.
$ sudo ap t-get i n s t a l l apache2 c u r l g i t lib m y s q lc lie n t- d e v m ysq l-server nodejs

У вас будет запрошен корневой пароль для вашего сервера MySQL. Если оста­
вить его поле пустым, он будет запрошен еще несколько раз. Если будет указан
пароль, его нужно будет использовать при создании базы данных, которое рас­
сматривается в главе 16, в разделе «Использование MySQL для создания базы
данных».
Поскольку в Ubuntu 11.04 нет предустановленного Ruby 1.9.2, нужно будет его
загрузить и встроить. По нашему мнению, для этого проще всего воспользоваться

1 h ttp s://rvm .b e g in re sc u e e n d .c o m /rv m /in sta ll


26 Часть I • Начало

диспетчером RVM. Установка RVM рассмотрена на веб-сайте RVM 1. В общих


чертах эти действия рассматриваются и в данной главе.
Сначала устанавливается сам RVM:
$ bash < < (c u r l -s h ttp s :/ / rv m .b e g in re s c u e e n d .c o m / in s ta ll/ rv m )

Следуйте инструкциям по установке, выдаваемым данной командой. В част­


ности, нужно добавить установочные инструкции RVM-функции в ваш файл
.bash_profile:
$ echo ' [ [ -s "$НОМЕ/. rv m /scrip ts/rvm " ] ] && \
. "$НОМЕ/.r v m / s c rip ts / rv m "' >> ~ / . bash_profile

Выйдите из окна командной строки или приложения Terminal и откройте их


новый экземпляр. Это приведет к перезагрузке с использованием вашего файла
.bash_profile.
Выполните следующую команду, которая предоставляет дополнительные ин­
струкции по установке применительно к вашей операционной системе:
$ rvm notes

Найдите строку, в которой рассказывается, как установить необходимые допол­


нения к операционной системе для работы с Ruby (MRI). После выполнения всех
этих инструкций можно перейти к установке самого интерпретатора Ruby:
$ rvm i n s t a l l 1 .9 .2

Предыдущий этап установки потребует времени на загрузку, конфигурирование


и компиляцию необходимых исполняемых файлов. Как только он будет завершен,
воспользуйтесь созданной средой и установите Rails:
$ rvm use 1 .9 .2
$ gem i n s t a l l r a i l s

За исключением инструкции rvm use, каждая из показанных выше инструкций


должна быть выполнена только один раз. Инструкцию rvm use нужно повторять
при каждом открытии окна оболочки. Ключевое слово use является необязатель­
ным, поэтому инструкцию можно сократить до вида rvm 1 .9 .2 , Можно также для
новых сеансов работы с терминалом сделать эту версию интерпретатора Ruby ис­
пользуемой по умолчанию, воспользовавшись следующей командой:
$ rvm - - d e fa u lt 1 .9 .2

Убедиться в успехе установки можно с помощью следующей команды:


$ r a ils -v

Если возникнут проблемы, воспользуйтесь рекомендациями, перечисленными


на веб-сайте rvm под заголовком «Troubleshooting Your Install»2.
Теперь, когда были рассмотрены инструкции для Windows, Mac OS X и Linux,
все последующие инструкции будут общими для всех трех операционных систем.

1 h ttp s://rv m .b e g in re sc u e e n d .c o m /rv m /in sta ll/


2 h ttp s://rv m .b e g in re sc u e e n d .c o m /rv m /in sta ll
Глава 1 • Установка Rails 27

1.4. Выбор версии Rails


Предыдущие инструкции помогли вам установить самую последнюю версию
Rails. Но порой именно самая последняя версия может и не понадобиться. Н а­
пример, может потребоваться запустить версию Rails, соответствующую той
версии, которая использовалась при создании данной книги, чтобы была уве­
ренность в том, что выводимые данные и примеры полностью соответствуют
изложенному материалу. Или, возможно, разработка ведется на одной машине,
но эксплуатация предполагается на другой, содержащей неподконтрольную вам
версию Rails.
Попав в какую-либо из подобных ситуаций, нужно быть в курсе нескольких
вещей. Сначала нужно с помощью команды gem найти все установленные версии
Rails:
$ gem l i s t - - lo c a l r a i l s

Также нужно с помощью команды r a i l s - - v e r s i o n проверить, какая версия


Rails запускается по умолчанию. Эта команда должна вернуть 3.1.0 или индекс
более свежей версии.
Установка другой версии Rails также осуществляется через команду gem. В за­
висимости от вашей операционной системы, может понадобиться предварить ее
командой sudo.
$ gem i n s t a l l r a i l s - -versio n 3 .1 .0

При наличии сразу нескольких версий Rails никто не сможет добиться нужного
результата, пока не будет способа выбора одной из них. И такой способ есть. Вызы­
вая любую команду r a i l s , можно указать, какая версия Rails будет использоваться,
вставив перед первым аргументом команды полный номер версии, окруженный
символами подчеркивания:
$ r a i l s _ 3 .1 .0 _ --version

Это особенно важно при создании нового приложения, поскольку, если при­
ложение создано с использование конкретной версии Rails, оно будет и дальше
продолжать использовать эту версию, даже если на системе установлены более
свежие версии, до тех пор, пока не будет принято решение обновить приложение.
Для обновления нужно будет просто указать номер новой версии в файле Gemfile,
который находится в корневом каталоге вашего приложения, и запустить коман­
ду b u n d le i n s t a l l . Более подробно эта команда будет рассмотрена в главе 25, в
разделе «Управление компонентами, от которых зависит работа приложения с
помощью системы Bundler».

1.5. Настройка среды разработки


Повседневная работа по созданию Rails-программ не отличается особым разноо­
бразием, но каждый работает по-своему. Мы это делаем следующим образом.
28 Часть I • Начало

Командная строка
Основную работу мы проделываем в командной строке. Хотя появляются все
новые и новые средства графического пользовательского интерфейса (G U I),
помогающие генерировать Rails-приложения и управлять ими, мы считаем,
что командная строка по-прежнему является наиболее эффективным способом
разработки. Поэтому вам стоит потратить немного времени на ознакомление
с командной строкой вашей операционной системы. Нужно выяснить, как она
используется для редактирования вводимых команд, как можно найти и отредак­
тировать предыдущие команды и как осуществлять автозавершение вводимых
имен файлов и команд.
Для оболочек Unix B a sh и z s h стандартным является автозавершение с по­
мощью клавиши табуляции (так называемое tab -завершение). Оно позволяет
набирать первые несколько символов имени файла, и нажатием клавиши Tab за­
ставлять оболочку провести поиск и завершить имя, используя соответствующие
имена файлов.

Управление версиями
Вся наша работа ведется в системе управления версиями (на данный момент в Git).
При создании нового Rails-проекта в Git заносится предмет проверки, и после
прохождения тестов туда передаются все изменения. Обычно передача данных
в репозиторий ведется по нескольку раз в час.

А ГДЕ ЖЕ IDE? --------------------------------------------------------------------------------------


Если вы занялись Ruby и Rails после того, как поработали с такими языками, как C# и
Java, у вас может появиться вопрос насчет IDE (интегрированной среды разработки). Ведь
всем известно, что невозможно создать код современного приложения без хотя бы 100
Мбайт IDE, поддерживаю щ его каждое нажатие клавиши. Всем, кто считает себя слишком
грамотным, мы предлагаем прямо сейчас усесться поудобнее и обложиться со всех сторон
грудами справочников по среде разработки и книгами по 1000 страниц на тему «как все
просто можно сделать».

Для Ruby или Rails пока не сущ ествует совершенных IDE (хотя некоторые среды разработки
уже на подходе). Вместо них большинство Rails-разработчиков использую т старые добрые
текстовые редакторы. Оказывается, не стоит драматизировать ситуацию. Используя дру­
гие, менее выразительные языки, программисты полагаются на IDE для того, чтобы она
делала за них большую часть рутинной работы: генерировала код, помогала осущ ествлять
навигацию по файловой системе и проводила постоянную компиляцию, моментально вы­
давая предупреждения об ошибках.

При работе с Ruby такая мощная поддержка просто ни к чему. Текстовые редакторы, на­
подобие TextMate, предоставят вам 90% всего, что вы получали от IDE, выступая при этом
в более легком весе.

Пожалуй, единственно полезной из утраченных функций IDE будет поддержка мгновенной


проверки кода.

Если работа над Rails-проектом ведется совместно с другими людьми, нужно по­
думать об установке распределенной системы интеграции —continuous integration
Глава 1 • Установка Rails 29

system (CI). Когда кто-нибудь зарегистрирует изменения, C I-система проверит


новую копию приложения и выполнит все тесты. Это самый простой способ не­
медленного обнаружения случайных повреждений. Можно также настроить свою
CI-систему на ее использование клиентами для апробации самой последней версии
вашего приложения. Такая прозрачность является отличной гарантией того, что
ваш проект не уйдет в сторону от запросов потребителей.

Редакторы
Мы создаем свои Rails-программы, используя редакторы для программистов. За
годы работы мы поняли, что различные редакторы хороши для работы с различ­
ными языками и средами. Например, Дэйв сначала написал эту главу, используя
редактор Emacs, поскольку его режим Filladapt он считает непревзойденным, когда
дело касается форматирования вводимого XML-кода. Сэм внес поправки в эту
главу, используя редактор Vim. Многие считают, что ни Emacs, ни Vim не являются
идеальными средствами для Rails-разработки, и предпочитают пользоваться редак­
тором TextMate. Выбор редактора, конечно, дело сугубо личное, но в редакторе для
Rails стоит все же поискать следующие свойства:
О Поддержку выделения синтаксиса Ruby и HTML. Она идеально подойдет
для файлов формата .erb (формат файлов Rails, включающих в себя вставки
фрагментов Ruby в HTML-код).
О Поддержку автоматических отступов и обратных отступов в исходном коде
Ruby. Это свойство не только улучшает эстетическое восприятие: создание
редактором отступов по мере ввода программы — это наилучший способ
отслеживания неправильных структурных вложений в программном коде.
Способность делать обратные отступы важна при пересмотре кода и пере­
мещении материала. (Очень удобной представляется способность редактора
TextMate обрабатывать обратные отступы после вставки кода из буфера.)
О Поддержку вставок обычных логических структур Ruby и Rails. Вам при­
дется вводить множество коротких методов, и, если IDE создает структуру
метода путем нажатия одной или двух клавишных комбинаций, вы сможе­
те сконцентрироваться на главном материале, находящемся внутри этой
структуры.
О Хорошую навигацию по файловой системе. Мы еще увидим, что Rails-
приложения состоят из множества файлов: только что созданное приложение
попадает в мир, состоящий из сорока шести файлов, разбросанных по трид­
цати четырем каталогам, — и это еще до создания конкретного наполнения
этого приложения.
Вам нужна среда, помогающая осуществлять быстрые переходы: вы будете
добавлять строчку к контроллеру для загрузки значения, переключаться на
представление и добавлять строчку, чтобы вывести это значение на экран,
а затем переключаться на тестирование, чтобы убедиться, что все сделано
правильно. Такие средства, как Блокнот, в которых для выбора каждого
редактируемого файла приходится работать с диалоговым окном File ►Open,
30 Часть I • Начало

вряд ли будут соответствовать этому требованию. Мы предпочитаем иметь


сочетание изображения дерева файлов на боковой панели, небольшого набо­
ра клавишных комбинаций, позволяющих найти по имени в дереве каталогов
файл (или файлы), и некоторых встроенных механизмов, «знающих», как
осуществлять переходы, скажем, между действием контроллера и соответ­
ствующим ему представлением.
О Автозавершение имен. В Rails часто встречаются довольно длинные имена.
Хороший редактор позволяет вводить первые несколько символов, а затем
предлагает одним нажатием клавиши ввести возможное продолжение.
Мы не решаемся рекомендовать какие-то конкретные редакторы, поскольку
сами по-настоящему пользовались всего лишь несколькими из них и ненароком
можем оставить чей-нибудь любимый редактор вне этого списка. Тем не менее,
чтобы помочь вам начать работу с чем-нибудь посерьезнее, чем Блокнот, мы смеем
предложить следующие редакторы:
О TextMate ( http://macromates.com/). Редактор Ruby и Rails, выбранный нами
для Mac OS X.
О Xcode 3.0 на Mac OS X с органайзером, удовлетворяющим большинству ва­
ших потребностей. Руководство, позволяющее приступить к работе с Rails на
Leopard, доступно по адресу http://developer.apple.com/tools/developonrailsleopard.
html.
О Для тех, кто при любых условиях предпочтет использование TextMate, но
вынужден работать иод Windows, вполне подойдет E-TextEditor (http://
e-texteditor.com /), предоставляющий «все возможности TextM ate под
Windows».
О Aptana RadRails ( http://www.aptana.com/products/radrails). Интегрированная
среда разработки Rails-приложений, работающая на Aptana Studio и Eclipse.
Она работает под Windows, Mac OS X и Linux. В 2006 году это средство за­
воевало премию как лучшее открытое средство разработки на основе Eclipse,
а в 2007 году этот проект стала курировать компания Aptana.
О NetBeans IDE 6.5 ( http://netbeans.org/features/ruby/index.html). Эта среда рабо­
тает в Windows, Mac OS X, Solaris и Linux. Доступна в пакете с поддержкой
Ruby или же в пакете, допускающем последующую загрузку Ruby. В допол­
нение к конкретной поддержке Rails 2.0, Rake-целей и миграций баз данных,
эта среда поддерживает графический мастер генератора кода Rails и быстрые
переходы из действий Rails к соответствующему им отображению.
О jEdit (http://www.jedit.org/). Полнофункциональный редактор с поддержкой
Ruby. Имеет встроенную поддержку внешних модулей.
О Komodo (http://www.activestate.com/komodo-ide). ID E , разработанная компа­
нией ActiveState для динамичных языков, включая Ruby.
О RubyMine ( http://www.jetbrains.com/ruby/features/index.html). Коммерческая
IDE-среда для Ruby бесплатно доступная для специальных образовательных
проектов и проектов с открытым кодом. Работает под Windows, Мае OS X
и Linux.
Глава 1 • Установка Rails 31

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


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

Рабочий стол
Мы не собираемся указывать вам, как следует оформлять Рабочий стол при работе
с Rails, но расскажем, как мы делаем это.
Чаще всего мы занимаемся написанием кода, запуском тестов и исследованием
работы своего приложения в браузере. Поэтому на нашем главном Рабочем столе
разработчика находятся в постоянно открытом состоянии окна редактора и брау­
зера. Мы также хотим отслеживать регистрационные записи, генерируемые при­
ложением, поэтому держим открытым еще и окно терминала. В нем для прокрутки
содержимого регистрационного файла по мере его обновления мы пользуемся
командой t a i l -f. Обычно это окно настроено на отображение информации очень
мелким шрифтом, чтобы оно занимало меньше места, но, как только будет замечено
появление чего-нибудь интересного, этот шрифт увеличивается, чтобы все можно
было как следует рассмотреть.
Нам также нужен доступ к документации Rails API, которая просматривается
в браузере. Во введении шла речь об использовании команды g e m _ s e rv e r для за­
пуска локального веб-сервера, содержащего Rails-документацию. Это, конечно,
удобно, но, к сожалению, Rails-документация разбросана по нескольким отдельным
справочным каталогам. Если у вас есть постоянное подключение к Интернету, вы
можете воспользоваться ресурсом http://api.rubyonrails.org, чтобы увидеть всю Rails-
документацию в одном месте.

1.6. Rails и базы данных


В данной книге при создании примеров использовалась база данных SQLite 3
(версии 3.7.4 или ближайших к ней версий). Если вы собираетесь придерживаться
нашего кода, то, наверное, проще всего будег также воспользоваться SQLite 3. Если
же будет принято решение об использовании какого-нибудь другого средства, это
также не составит проблем. Возможно, для этого придется внести небольшие по­
правки в какой-нибудь явно выраженный SQL-фрагмент вашего кода, но Rails в
своих приложениях старается максимально избавиться от специфичного для баз
данных SQL-кода.
Если нужно будет подключиться к базе данных, отличной от SQLite 3, Rails
также работает с DB2, MySQL, Oracle, Postgres, Firebird и SQL Server. Для всех баз
данных, кроме SQLite 3, потребуется установить драйвер базы данных, библиотеку,
которую Rails сможет использовать для подключения и использования вашего
процессора базы данных. В этом разделе содержатся ссылки на инструкции, по­
зволяющие справиться с этой задачей.
Все драйверы баз данных написаны на С и распространяются в основном в виде
исходного кода. Если вам не хочется возиться с созданием драйвера из исходного
32 Часть I • Начало

кода, присмотритесь повнимательнее к веб-сайту драйвера. В большинстве случаев


обнаружится, что автор распространяет также и двоичные версии.

СОЗДАНИЕ СВОЕЙ СОБСТВЕННОЙ ДОКУМ ЕТАЦИИ RAILS A P I ------------------------


Вы можете создать собственную локальную версию сводной документации Rails API. Для
этого нужно ввести в командную строку следую щий набор команд:

ra ils_a p p s > r a i l s new dummy_app


ra ils_a p p s > cd dummy_app
dummy_app> rake d o c : r a il s

На последней стадии следует немного подождать. Когда все закончится, у вас будет до­
кументация Rails API в дереве каталогов, начинающ емся с doc/api. Я советую переместить
эту папку на Рабочий стол, а затем удалить дерево dum m y_app.
Для просмотра документации Rails API откройте в своем браузере страницу doc/api/index.
html.

Если вы не сможете найти двоичную версию или если вы все равно хотите
создать драйвер из исходного кода, вам для его создания понадобится среда раз­
работки, установленная на вашей машине. При работе под Windows это означает,
что у вас должна быть копия Visual C++. При работе под Linux вам понадобится
gcc и сопутствующие компоненты (которые, скорее всего, уже установлены).
При работе под OS X вам понадобится установить инструментарий разработ­
чика (который поставляется вместе с операционной системой, но по умолчанию не
устанавливается). Вам также понадобится установить драйвер вашей базы данных
в подходящую версию Ruby. Если вы установили свою собственную копию Ruby в
обход встроенной копии, важно помнить, что при создании и установке драйвера
базы данных эта версия Ruby должна быть указана в пути поиска первой. Чтобы
убедиться, что Ruby не запускается из каталога /usr/bin, можно воспользоваться
командой w h ic h ruby.
В следующем списке перечислены все доступные адаптеры баз данных и при­
ведены ссылки на веб-страницы, где они могут быть найдены:

DB2 http://raa.ruby-lang.org/project/ruby-db2 или http://rubyforge.org/projects/rubyibm

Firebird http://rubyforge.org/projects/fireruby/
MySQL http://w ww .tm tm .org/en/m ysql/ruby/

Oracle http://rubyforge.org/projects/ruby-oci8
Postgres http://rubyforge.org/projects/ruby-pg

Server https://github.com /rails-sqlserver SQL

SQLite http://rubyforge.org/projects/sqlite-ruby

В списке указана фирменная Ruby-версия адаптера Postgres. Версию postgres-pr


можно загрузить с веб-страницы Ruby-DBI, размещенной по адресу http://rubyforge.
o rg /p ro je c ts / ru b y - d b i.
Адаптеры MySQL и SQLite также можно загрузить в виде RubyGems (соот­
ветственно, mysql и sqlite).
Глава 1 • Установка Rails 33

1.7. Наши достижения


0 Установили (или обновили) интерпретатор языка Ruby.
И Установили (или обновили) среду Rails.
0 Установили (или обновили) базу данных SQLite3.
И Выбрали редактор.
Теперь, имея установленную среду Rails, давайте перейдем к ее использованию.
Настало время приступить к чтению следующей главы, где будет создано наше
первое приложение.
Немедленное
использование

О сн о в н ы е тем ы :

> со здание нового прилож ения;


> запуск сервера;
> об ращ ение к серверу из браузера;
> генерирование ди нам и ческого контента;
> д об авлен ие гипертекстовы х ссылок;
> передача д анны х из контроллера в представление.

Давайте напишем простое приложение, чтобы проверить, удачно ли среда Rails


была установлена на наших машинах, а также получить представление о работе
Rails.

2,1. Создание нового приложения


При установке среды Rails вы также получаете новый инструмент командной
строки, r a i l s , который используется для конструирования каждого нового соз­
даваемого вами Rails-приложения.
А зачем для этого требуется какой-то инструмент? Почему бы для этого просто
не приспособить наш любимый текстовый редактор и не создать исходный код
для приложения с нуля? Можно было бы, конечно, заняться и этой нудной рабо­
той. В конце концов, Rails-приложение — это всего лишь исходный код на языке
Ruby. Но Rails, кроме всего прочего, творит множество скрытых чудес, заставляя
наши приложения работать при минимуме явных настроек. Чтобы заставить Rails
Глава 2 • Немедленное использование 35

творить чудеса, ей нужно дать возможность отыскать все разнообразие компонен­


тов вашего приложения. Далее (в разделе 18.1 «Где что искать») станет ясно, что
для этого нужно создать конкретную структуру каталогов, разложив создаваемый
нами код по надлежащим местам. Команда r a i l s просто создает для нас эту струк­
туру каталогов и заполняет ее стандартным Rails-кодом.
Для создания нашего первого Rails-приложения откройте окно командной
оболочки и перейдите в то место файловой системы, в котором вы хотите создать
структуру каталогов для своих приложений. В нашем примере мы будем создавать
приложения в каталоге под названием work. Находясь в этом каталоге, введите ко­
манду r a i l s для создания приложения с названием demo. Имейте в виду: если у вас
уже существует каталог с таким названием, вам будет задан вопрос, не хотите ли
вы перезаписать все существующие в нем файлы.

П РИ М Е Ч А Н И Е----------------------------------------------------------------------------------------
Если, как описывалось в разделе 1.4 «Выбор версии Rails» нужно указать, какую из версий
Rails использовать, то это следует сделать именно здесь.

rubys> cd work
work> r a i l s new demo
c re a te
c re a te README
c re a te Rakefile
c re a te config.ru

c re a te ven d o r/p lugin s


c re a te ve n d o r/ p lu g in s / . g itk eep
run bundle i n s t a l l
F e tc h in g source index f o r h ttp ://rub yg em s.org /

Your bundle is com plete!


Use 'b u n d le show [gemname]' to see where a bundled gem i s in s t a l le d .
work>

Команда создала каталог по имени demo. Зайдите в него и выведите его содер­
жимое (используя I s в окне Unix или d i r при работе под Windows).
Вы должны увидеть набор файлов и подкаталогов:
work> cd demo
demo> d i r /w
[• ] [••] . g itig n o r e [app] [config]
config. ru [db] [doc] Gemfile Gem file.lock
[lib ] [ lo g ] [p u b lic ] Rakefile README
[s c r ip t ] [te s t] [trnp] [vend or]

Поначалу все эти каталоги (и файлы, которые в них содержатся) могут отпуг­
нуть, но на данный момент на существование большинства из них можно просто не
обращать внимания. В данной главе непосредственно будет использоваться только
один из них: каталог арр, в котором будет создаваться наше приложение.
36 Часть I • Начало

Включенные туда файлы представляют собой все, что нужно для запуска ав­
тономного веб-сервера, способного выполнять наше только что созданное Rails-
приложение. Давайте без лишних проволочек запустим наше приложение под
названием demo:
demo> r a i l s server
=> Bo o tin g W EBrick
=> R a ils 3 .1 .0 a p p lic a tio n s t a r t in g on h t t p :/ / 0 .0 .0.0:3000
=> C a ll w ith -d to detach
=> C trl- C to shutdown s e rv e r
[2011-07-23 10:38:18] INFO W EBrick 1 .3 .1
[2011-07-23 10:38:18] INFO ruby 1 .9 .2 (2011-05-12) [x8 6_6 4-linux]
[2011-07-23 10:38:18] INFO W E B ric k : : H T T P S e rv e r# s ta rt: pid=6044 port=3000

Какой веб-сервер запущен, зависит от того, какой сервер установлен. W E ­


Brick — веб-сервер, специально предназначенный для Ruby, который распро­
страняется вместе с Ruby и поэтому является гарантированно доступным. Но,
если на вашей системе установлен другой веб-сервер (и Rails может его найти),
команда r a i l s s e r v e r может предпочесть его серверу W EBrick. Rails можно
заставить воспользоваться WEBrick, предоставив команде r a i l s соответствую­
щий ключ:
demo> r a i l s server webrick

В последней строчке трассировка запуска показывает, что мы только что за­


пустили веб-сервер с использованием порта 3000. Часть адреса 0.0.0.0 означает,
что W EBrick допустит подключения ко всем интерфейсам. На установленной
у Дэйва системе OS X это означает доступ к обоим локальным интерфейсам
(127.0.0.1 и :: 1) и его подключению к локальной сети. Мы можем получить доступ
к приложению, указав веб-браузеру URL-адрес http://localhost:3000. Результат
показан на рис. 2.1.
Если смотреть в окно, из которого был запущен сервер, вы увидите трасси­
ровку, показывающую, что вы запустили приложение. Мы собираемся оставить
сервер работать в этом консольном окне. Позже, после того как вы напишете код
приложения и оно будет запущено в нашем браузере, у нас будет возможность вос­
пользоваться этим консольным окном для отслеживания входящих запросов. Когда
наступит время выключить ваше приложение, можно будет, находясь в этом окне,
нажать Ctrl+C чтобы остановить WEBrick (не делайте этого сейчас, через минуту
мы именно этим приложением и воспользуемся).
Итак, у нас есть запущенное новое приложение, но в нем вообще нет нашего
кода. Давайте исправим эту ситуацию.

2.2. Привет, Rails!


Ничего не поделаешь, для испытания новой системы придется написать програм­
му Hello, World! Начнем с создания простейшего приложения, отправляющего на
Глава 2 • Немедленное использование 37

браузер наше радостное приветствие. Как только оно заработает, мы украсим его
показом текущего времени и ссылками.
В главе 3 «Архитектура Rails-приложений» мы выясним, что Rails относится
к среде Модель—Представление—Контроллер (Model—View—Controller). Rails по­
лучает входящие запросы от браузера, декодирует запрос для поиска контроллера
и вызывает в контроллере метод, который называется действием. Затем контроллер
вызывает соответствующее представление, отображающее результаты на экране
пользователя. Для нас Rails хороша тем, что берет на себя управление большей
частью внутренних каналов, связывающих все эти действия. Чтобы создать про­
стейшее приложение Hello, World!, нам нужен код для контроллера и представления
и нужен связывающий их маршрут. Для модели нам код не нужен, поскольку мы
не имеем дела с данными. Намнем с контроллера.
Затем контроллер вызовет конкретное представление, чтобы пользователь мог
увидеть результаты.

B r o w s e th e
Welcome aboard d o c u m e n ta tio n

A b o u t v o u r a p p li c a ti o n 's e n v ir o n m e n t R a ils G u id es
R a ils API

R u b y core

Getting started Rubv standard lib ra ry


Here's how to get rolling;

1. Use r a i l s g e n e r a t e to create your models and


controllers
T o se e all a v a ila b le o p tio n s , run it w ith ou t param eters.

Set up a default route and remove public/index, html

Create vour database


Run r a k e d b : c r e a t e to create y o u r d atab ase. It you're not u sin g
S Q L ite (the d efau lt), ed it c o n fig d a ta b a s e .y m l w ith you r u sernam e and
passw ord.

Рис. 2.1. Только что созданное Rails-приложение


38 Часть I • Начало

Аналогично тому, как для создания нового Rails-ириложения мы воспользова­


лись командой r a i l s , для создания нового контроллера для нашего проекта мы
можем также воспользоваться генерирующим сценарием. Он вызывается коман­
дой n a i l s g e n e r a t e . Итак, для создания контроллера под названием s a y нужно
убедиться, что мы находимся в каталоге demo, и запустить команду, передав ей имя
создаваемого контроллера и имена действий, предназначаемых для поддержки
этого контроллера:
demo> r a i l s generate c o n t r o lle r Say h e llo goodbye
c re a te a p p / c o n tr o lle r s / s a y _ c o n tr o lle r .r b
ro u te get "say/goodbye"
ro u te get "s a y / h e llo "
invoke erb
c re a te app/views/'say
c re a te a p p / v ie w s / s a y / h e llo .h tm l.e rb
c re a te app/view s/say/goodbye. h tm l. erb
invoke t e s t _ u n it
c re a te t e s t / f u n c t io n a l/ s a y _ c o n t r o lle r _ t e s t . rb
invoke h e lp e r
c re a te a p p / h e lp e rs / s a y _ h e lp e r. rb
invoke t e s t _ u n it
c re a te t e s t / u n it / h e lp e r s / s a y _ h e lp e r _ t e s t . rb
invoke a s se ts
invoke c o ffe e
c re a te a p p / a s s e t s / ja v a s c r ip t s / s a y .js . c o ffe e
invoke scss
c re a te a p p / a s s e ts / s ty le s h e e ts / s a y . c s s .s c s s

Команда r a i l s g e n e r a t e ведет протокол проверяемых файлов и каталогов,


отмечая добавление новых Ruby-сценариев или каталогов к вашему приложению.
На данный момент нас интересует один из этих сценариев, а еще через минуту за­
интересуют и файлы .html.erb.
Сначала нужно найти исходный файл контроллера. Он находится в файле арр/
controllers/say_controller.rb. Посмотрим на его содержимое:

rails31 /w ork/d em ol/ap p/co ntro llers/say_co ntro ller.rb


c la s s S a y C o n tro lle r < A p p lic a tio n C o n t r o lle r
d e f h e llo
end

d e f goodbye
end

Довольно лаконично, не правда ли? S a y C o n t r o l l e r — это класс, наследуемый


из A p p l i c a t i o n C o n t r o l l e r , поэтому он автоматически перенимает все исходное
поведение контроллера. Что этот код должен делать? Пока он не делает ничего,
у нас просто есть пустой метод действия по имени h e l l o . Чтобы понять, почему
он гак назван, нужно посмотреть, как Rails обрабатывает запросы.
Глава 2 • Немедленное использование 39

Rails и запросы URL-адресов


В представлении своих пользователей Rails-приложение, как, собственно, и любое
другое веб-приложение, связано с URL-адресом. Когда браузеру указывается этот
URL-адрес, происходит обращение к коду приложения, который генерирует для
вас ответ.
Давайте попробуем прямо сейчас. Перейдите в окне браузера на URL-адрес
http://localhost:3000/say/hello. (Заметьте, что в разрабатываемом приложении в на­
чале пути отсутствует какая-либо строка, указывающая на само приложение, —■мы
обращаемся непосредственно к контроллеру.) Вы увидите на экране окно, похожее
на то, которое показано на рис. 2.2.

Рис. 2.2. Шаблон, подготовленный для нашего заполнения

Наше первое действие


На данный момент мы можем увидеть не только ответ на вопрос, подключен ли
URL-адрес к нашему контроллеру, но также и то, что Rails указывает путь к на­
шему следующему шагу, а именно — сообщению Rails о том, что отобразить на
экране. Здесь в игру вступают представления. Вспомните: когда мы запустили
сценарий для создания нового контроллера, эта команда добавила к нашему
приложению шесть файлов и новый каталог. В этом каталоге содержатся файлы
шаблонов для представлений контроллера. В нашем случае мы создали контрол­
лер, названный say , следовательно, представления будут находиться в каталоге
app/views/say.
По умолчанию Rails ищет шаблоны в файле с тем же именем, что и у действия,
занимающегося его обработкой. В нашем случае это означает, что нам нужно заме­
нить файл по имени hello.html.erb в каталоге app/views/say. (Почему .html.erb? Совсем
скоро мы все объясним.) А теперь давайте просто поместим в файл элементарный
код HTML:
rails31/w ork/dem ol/app/view s/say/hello. htm l. erb
<Ь1>Привет от R a ils !< / h l>

Сохраните файл hello.html.erb и обновите окно браузера. Вы увидите в нем наше


приветствие:
40 Часть I • Начало

шж тшштшь&шшшшш

С ti О localhost ☆ -ч

Привет от Rails!

Всего мы просмотрели два файла в нашем дереве Rails-приложения. Мы смотре­


ли на контроллер, и мы изменили шаблон отображения страницы в браузере.
Эти файлы находятся в стандартных местах иерархической структуры Rails:
контроллеры размещаются в каталоге app/controllers, а представления — в подката­
логах, принадлежащих каталогу app/views. Стандартные места для контроллеров
и представлений показаны на рис. 2.3.

class SayController < ApplicationController


def hello
end
end

I— sa y _ c o n tro lle r.rb

— m o d e ls /

__ v ie w s / <h1>Hello from Rails!</h1>


|— say/ ' ..................
I— h e llo .h tm l.e rb

Рис. 2.3. Стандартные места для контроллеров и представлений

Придание динамичности
Пока у нашего Rails-приложения довольно скучный вид —оно просто отображает
статичную страницу. Для придания ему динамичности давайте заставим его по­
казывать текущее время при каждом отображении страницы.
Для этого необходимо внести изменения в файл шаблона в представлении: те­
перь в него нужно включить время в виде строки. При этом возникает два вопроса.
Во-первых, как добавляется динамическое содержимое к шаблону? И во-вторых,
откуда мы возьмем это время?

Динамическое содержимое
В Rails есть множество способов создания динамических шаблонов. Наиболее рас­
пространенный из них, которым мы здесь воспользуемся, заключается во вставке
Глава 2 • Немедленное использование 41

кода Ruby непосредственно в шаблон. Именно поэтому наш файл шаблона назы­
вается hello.html.erb —суффикс .html.erb предписывает Rails расширить содержимое
файла с помощью системы, которая называется ERB.
ERB — это фильтр, устанавливаемый как часть Rails, который берет файл
.erb и выдает преобразованную версию. Выходной файл в Rails чаще всего имеет
формат HTML, но вообще-то может иметь какой угодно формат. Обычно содер­
жимое пропускается через фильтр без изменений. А содержимое, находящееся
между группами символов <%= и %>, интерпретируется как Ruby-код, который
выполняется. В результате этого выполнения содержимое превращается в стро­
ку, значение которой подставляется в файл вместо последовательности <%=...%>.
Например, внесите в hello.html.erb изменения, позволяющие вывести текущее
время:

rails31/work/demo2/app/views/say/hello. htm l. erb


<|11>Привет от R a i l s ! </hl>
►<р>
► Сейчас <%= Time.now %>
►</р>

После обновления окна нашего браузера мы увидим время, отображенное с ис-


стандартного формата Ruby1:

1<= !®
(^} Demo

^ С Й © lo c a l h o s t :00CVsay. h e llo ☆ Л

Привет от Rails!
С ей ч ас 2 0 1 1 -1 0 -0 9 13:21:13 + 0 3 0 0
1

Обратите внимание: при щелчке на кнопке браузера Обновить (Refresh) время


обновляется при каждом выводе страницы. Это похоже на настоящую генерацию
динамического содержимого.

Добавление времени
Исходная задача заключалась в демонстрации времени пользователям нашего
приложения. Теперь мы знаем, как заставить приложение выводить динамические
данные. Но нужно решить еще одну задачу: откуда брать время?
Мы показали, что подход со вставкой вызова Ruby-метода T im e .n o w ( ) в на­
шем шаблоне hello.htm l.erb работает. При каждом обращении к этой странице

1 В Ruby 1.8.7 стандартный формат был другим и выглядел примерно следующим об­
разом: Mon Jul 25 09:43:25 -0400 2011.
42 Часть I • Начало

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

rails31/w ork/dem o3/app/controllers/say_controller.rb


c la s s S a y C o n tr o lle r < A p p lic a tio n C o n t r o lle r
d e f h e llo
► @time = Time.now
end
d ef goodbye
end
end

В шаблоне .html.erb мы воспользуемся этой переменной экземпляра, чтобы под­


ставить время в выводимые данные:
rails31/work/dem o3/app/views/say/hello.htm l.erb
<|-|1>Привет от R a ils ! </hl>
<Р>
► Сейчас <%= (Stime %>
</р>

После обновления окна браузера мы опять увидим текущее время, что свиде­
тельствует об успешной связи между контроллером и представлением.

УПРОЩЕНИЕ РАЗРАБОТКИ------------------------------------------------------------------
В процессе той разработки, которой мы только что занимались, уж е можно было кое-что
заметить. Когда к уж е работающ ему приложению добавлялся какой-нибудь код, его не при­
ходилось перезапускать. Все необходимое делалось без нашего участия в фоновом режиме.
И каждое вносимое нами изменение становилось доступным, как только мы обращ ались
к приложению через браузер. Благодаря чему все это происходило?
Оказывается, благодаря достаточно разумному поведению диспетчера Rails. В режиме раз­
работки (в отличие от режимов тестирования или эксплуатации) при поступлении нового
запроса диспетчер автоматически перезагруж ает исходные файлы приложения. Таким об­
разом, при редактировании приложения диспетчер обеспечивает запуск самой последней
версии. Для режима разработки это очень хорош ее качество.
Тем не менее за такую гибкость поведения приходится платить — она приводит к неболь­
шой паузе между вводом URL-адреса и откликом приложения, которая вызвана тем, что
диспетчер осущ ествляет перезагрузку. Для разработки это вполне разумная цена, но при
эксплуатации готового приложения такой режим будет неприемлем. Поэтому перед раз­
вертыванием приложения этот режим отклю чается (см. главу 16 «Задача Л: развертывание
и эксплуатация»).

Зачем нам понадобились дополнительные хлопоты по установке отображаемого


времени в контроллере с последующим использованием этого времени в пред­
ставлении? Хороший вопрос. В данном приложении нет практически никакой
разницы, но, помещая логику работы приложения из представления в контроллер,
Глава 2 • Немедленное использование 43

мы получаем некоторые преимущества. Например, в будущем может потребоваться


усовершенствовать наше приложение, чтобы оно поддерживало работу пользова­
телей во многих странах. В таком случает понадобится локализовать отображение
времени, выбрав время, соответствующее часовому поясу пользователей. Для
этого может понадобиться достаточно большой объем кода на уровне приложе­
ния. и, возможно, его вставка на уровне представления окажется неприемлемой.
Устанавливая значение отображаемого времени в контроллере, мы придаем своему
приложению дополнительную гибкость —теперь мы можем изменять часовой пояс
в контроллере, и тогда уже не придется обновлять каждое представление, в котором
используется этот объект времени. Время —это данные, которыми контроллер дол­
жен снабжать представление. При согласовании моделей вам будет представлено
намного больше подобных примеров.

Итак, чего мы добились


Рассмотрим вкратце работу созданного приложения.
1. Пользователь переходит к работе с приложением, воспользовавшись в нашем
случае локальным URL-адресом http://localhost:3000/say/he!lo.
2. Затем Rails проводит сравнение со схемой маршрута, которая предваритель­
но разбивается на две части.
Фрагмент say воспринимается как имя контроллера, поэтому Rails создает
новый экземпляр Ruby-класса S a y C o n t r o l l e r (который находит в файле
app/controllers/say_controller.rb).
3. Следующий фрагмент URL, hello, рассматривается как идентификатор дей­
ствия. Rails вызывает в контроллере метод с таким же названием. Этот метод,
определяющий действие, создает новый объект Тime, содержащий текущее
время, и убирает его содержимое в переменную экземпляра @time.
4. Rails ищет шаблон, чтобы отобразить результат. Для этого просматривается
каталог app/views, в котором идет поиск подкаталога с именем, аналогичным
имени контроллера (say), а в этом подкаталоге ищется файл, названный по
имени действия (hello.html.erb).
5. Rails обрабатывает этот файл с использованием системы работы с шаблонами
ERB, исполняя все Ruby-вставки и подставляя значения, установленные
контроллером.
6. Результат возвращается браузеру, и Rails завершает обработку данного за­
проса.
Но это еще не все. Rails предоставляет вам массу возможностей для внесения
изменений в основную последовательность выполняемых действий (и мы отча­
сти этим воспользуемся). В данных условиях наша история является типичным
примером использования соглашения по конфигурации, одной из основ фило­
софии Rails. Благодаря тому, что Rails-приложениям предоставляются удобные
исходные настройки, а при их разработке применяются определенные соглашения
по составлению URL-адреса или по файлу, в который помещается определение
контроллера, а также по имени используемого класса и именам методов, Rails-
44 Часть I • Начало

приложения создаются, как правило, с незначительным использованием или вовсе


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

2.3. Соединение страниц


Веб-приложения, состоящие из одной страницы, встречаются довольно редко.
Давайте посмотрим, как к нашему приложению с приветствием можно добавить
еще один впечатляющий пример веб-дизайна.
Обычно каждой странице приложения соответствует отдельное представление.
В нашем случае для обработки страницы также будет использован новый метод
действия (хотя, далее в данной книге будет показано, что так бывает не всегда).
Для обоих действий будет использован один и тот же контроллер. Вообще-то, так
делать не обязательно, но каких-то причин, вынуждающих использовать новый
контроллер, пока нет.
Для этого контроллера уже было определено действие goodbye, поэтому остает­
ся только создать новый шаблон в каталоге app/views/say. На этот раз он называется
goodbye.html.erb, поскольку по умолчанию шаблоны получают имена тех действий,
которые с ними связаны.

rails31/work/demo4/app/views/say/goodbye. htm l. erb


<И1>До свидания! </hl>
<р>
Очень приятно, что вы нас навестили.

Обратимся еще раз к нашему уже испытанному браузеру, но теперь уже укажем
на новое действие, воспользовавшись URL http://localhost:3000/say/goodbye. Должна
появиться картинка, похожая на эту:


Q Demo

О Л О lo c a l h o s t ☆ \]

До свиданья!
I О чень п р иятно, ч то вы нас навестили.

Теперь нам нужно связать эти два экрана. Мы поместим на страницу hello ссыл­
ку, которая приведет нас на страницу goodbye, и наоборот. В настоящем приложении
для этого, скорее всего, понадобится создать соответствующие кнопки, но сейчас
мы воспользуемся простыми гиперссылками.
Нам уже известно, что в Rails используется соглашение о синтаксическом раз­
боре URL-адреса с целью получения целевого контроллера и имеющиеся внутри
Глава 2 • Немедленное использование 45

него действия. Поэтому проще всего будет воспользоваться этим URL-соглашением


для наших ссылок.
В файле hello.html.erb будет содержаться следующий код:

<р>
С к а за ть <а href="/say/goodbye">flo с в и д а н ь ж / а > !
</р>

А в файле good bye. html. erb будет указатель на обратный путь:

<Р>
С к а за ть <а href= "/say/hello"> npnBeT< /a> !
</р>

Конечно, все это будет работать, но сам по себе этот код слишком несовершенен.
Если придется перемещать наше приложение в другое место на веб-сервере, URL-
адреса уже не будут указывать на правильное место. К тому же в наш код также
закладываются предположения об используемом в Rails формате URL-адреса,
который в будущих версиях Rails может измениться.
К счастью, мы можем обойтись без всякого риска. Rails поставляется с целым
пакетом вспомогательных методов, которые могут использоваться в шаблонах
представлений. В данном случае мы воспользуемся вспомогательным методом
l i n k _ t o ( ), создающим гиперссылку на действие. (Метод l i n k _ t o ( ) способен на
большее, но пока ограничимся только этой возможностью.) При использовании
l i n k _ t o ( ), hello.html.erb приобретает следующий вид:

rails31/work/dem o5/app/views/say/hello.htm l.erb


<И1>Привет от R a i l s ! </hl>
<Р>
Сейчас <%= @time %>
</р>

► Пора с к а з а т ь
► <%= lin k _ t o "до св и д а н и я ", say_goodbye_path %>!
►</р>

Здесь мы видим вызов метода l i n k _ t o ( ) внутри ERB-последовательности


<%=...%>. За счет него создается ссылка на URL-адрес, который вызовет действие
goodbye(). Первым аргументом вызова l i n k _ t o ( ) является текст, отображаемый
в гиперссылке, а второй аргумент заставляет Rails сгенерировать ссылку на дей­
ствие goodbye.
Давайте уделим пару минут рассмотрению порядка генерации ссылки. Мы на­
писали следующий код:
lin k _ t o "до с в и д а н и я ", say_goodbye_path

Начнем с того, что lin k _ to — это вызов метода. (В Rails методы, облегчающие
написание шаблонов, называются помощниками.) Если вам приходилось работать
46 Часть I • Начало

с такими языками, как Java, отсутствие обязательных круглых скобок вокруг ар­
гументов метода может вызвать удивление. Но если они вам нравятся, их всегда
можно добавить.
Фрагмент s a y _ g o o d b y e _ p a t h является заранее вычисленным значением,
которое Rails делает доступным для представлений приложения. Этот фрагмент
превращается в путь /say/goodbye. Со временем вы увидите, что Rails предоставляет
возможность дать имена всем маршрутам, которые будут использоваться в вашем
приложении.
А теперь вернемся к приложению. Если направить браузер на открытие нашей
страницы hello, то на ней будет ссылка на страницу goodbye:

1.с 1 .ё ) № Э м (
О Demo

С ’ lo c a lh o s t & Л

Привет от Rails!
С ейч ас

П ора сказать д о свидания!

Соответствующие изменения могут быть сделаны и в файле goodbye.html.erb,


чтобы появилась ссылка на начальную страницу hello:

rails31/work/demo5/app/views/say/goodbye.htm l. erb
<И1>До свиданья! </hl>
<P>
Очень приятно, что вы нас навестили.
</р>

► Сказать <%= lin k _ t o "п р и вет", s a y _ h e llo _ p a th %> еще раз.


►</р>

Вот мы и завершили разработку нашего пробного приложения, проверив тем


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

2.4. Наши достижения


Мы создали пробное приложение, увидев при этом:
0 как создается новое Rails-приложение и как создается новый контроллер
этого приложения;
Глава 2 • Немедленное использование 47

0 как в контроллере создается динамическое содержимое и как оно отобража­


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

Чем заняться на досуге


Поэкспериментируйте со следующими выражениями:
О Сложение: <%= 1+2 %>
О Объединение: <%= "cow" + "b o y " %>
О Время через час: <%= 1. h o u r . f rom_now %>
Вызов следующего Ruby-метода возвращает список всех файлов в текущем
каталоге:
@files = D i r . g l o b ( ' * ' )

Воспользуйтесь им для задания значения переменной экземпляра в дей­


ствии контроллера, а затем напишите соответствующий шаблон, выводящий
в браузере список имен файлов.
Подсказка: просканировать коллекцию можно с помощью следующего
кода:
<% f o r file in (Sfiles %>
file name i s : <%= file %>
<% end %>

Для формирования списка можно воспользоваться тегом <ul>.


(Подсказки можно найти по адресу http://www.pragprog.com/wikis/wiki/RaiisPlay
Time.)

Ликвидация последствий
Если вы, следуя за повествованием, набирали код, то, возможно, приложение все
еще работает на вашем компьютере. Когда приблизительно через десять страниц мы
приступим к программированию нашего следующего приложения, при его первом
запуске возникнет конфликт, поскольку для общения с браузером оно также по­
пытается воспользоваться компьютерным портом 3000. Сейчас как раз настало
время остановить текущее приложение, нажав комбинацию клавиш Ctrl+C в том
окне, которое использовалось для его запуска.
А теперь перейдем к обзору Rails.
Архитектура Rails-
приложений

Основные темы:
> модели;
> представления;
> контроллеры .

Одной из существенных особенностей Rails является то, что она накладывает


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

3.1. Модели, представления и контроллеры


В далеком 1979 году Трюгве Реенскауг (Trygve Reenskaug) придумал новую архи­
тектуру для разработки интерактивных приложений. По его замыслу приложения
разбиваются на компоненты трех типов: модели, представления и контроллеры.
Модель отвечает за поддержку состояния приложения. Иногда это состояние
является кратковременным, продолжающимся только на время нескольких взаимо­
действий с пользователем. А иногда состояние является постоянным и сохраняется
вне приложения, чаще всего в базе данных.
Модель — это не просто данные; в ней прописаны все бизнес-правила, приме­
няемые к этим данным. К примеру, если скидка не должна применяться к заказам
стоимостью менее 20 долларов, моделью будут предписаны соответствующие
ограничения. И в этом есть свой определенный смысл. Помещая реализацию
бизнес-правил в модель, мы гарантируем, что ничто иное в приложении не может
Глава 3 • Архитектура Rails-приложений 49

сделать наши данные некорректными. Модель работает и сторожем, и хранителем


данных.
Представление отвечает за формирование пользовательского интерфейса, ко­
торый обычно основан на данных модели. Например, интернет-магазин должен
иметь перечень товаров для отображения на экране каталога. Этот перечень будет
доступен через модель, но получать его у модели и форматировать для показа ко­
нечному пользователю будет представление. Хотя представление может предлагать
пользователю различные способы ввода данных, оно никогда не занимается их не­
посредственной обработкой. Работа представления завершается, как только данные
будут отображены на экране. Доступ к одним и тем же данным модели, зачастую
с разными целями, может иметь множество представлений. В интернет-магазине
может быть представление, отображающее информацию о товаре на странице
каталога, и другой набор представлений, используемых администраторами для
добавления товаров или редактирования сведений о них.
Контроллеры организуют работу приложения. Они воспринимают события
внешнего мира (обычно ввод данных пользователем), взаимодействуют с моделью
и отображают соответствующее представление для пользователя.
Этот триумвират —модель, представление и контроллер —формирует архитек­
туру, известную как MVC (M odel—View—Controller — Модель—Представление-
Контроллер). Взаимодействие этих трех элементов концепции показано на
рис. 3.1.

Б раузер отправляет зап рос


Г 'I; Контроллер вза им од ей ств ует с м од ел ью

I
3 Контроллер вы зы вает представление

Контроллер &■ П редставление вы водит сл ед у ю щ и й


экран браузера

База
П редставление Модель
данн ы х

Рис. 3.1. Архитектура Модель— Представление— Контроллер

Сначала архитектура MVC предназначалась для обычных G U I-приложений,


при создании которых разработчики поняли, что распределение ролей приводит
к существенному уменьшению взаимных увязок, что в свою очередь облегчает
написание и сопровождение программного кода. Каждое понятие или действие
четко определялось только в одном, хорошо известном месте. Использование
MVC напоминало строительство небоскреба при наличии уже готового каркаса:
когда есть готовая структура, навесить на нее все остальные элементы намного
проще. В процессе разработки нашего приложения мы часто будем пользоваться
возможностями Rails по генерации для нашего приложения временных платформ
(scaffold).
50 Часть I • Начало

Ruby on Rails также относится к среде MVC. Rails навязывает структуру для
вашего приложения — вы разрабатываете модели, представления и контроллеры
как отдельные функциональные блоки, a Rails при выполнении вашей программы
связывает их вместе. Изюминкой Rails является то, что процесс увязки базируется
на использовании разумных умолчаний, которые, как правило, избавляют вас от
написания каких-либо внешних конфигурационных метаданных, обеспечивающих
взаимную работу. Приоритет соглашения над конфигурацией является примером
философии Rails.
В Rails-приложении входящий запрос сначала посылается маршрутизатору,
который решает, в какое место приложения должен быть отправлен запрос и как
должен быть произведен синтаксический разбор этого запроса. В конечном итоге
на данном этапе где-то в коде контроллера идентифицируется конкретный метод
(называемый в Rails действием). Действие может искать запрошенные данные,
может взаимодействовать с моделью и может вызвать другое действие. В результате
выполнения действие подготавливает информацию для представления, которое
создает изображение для пользователя.
Rails обрабатывает входящие запросы по схеме, показанной на рис. 3.2. В дан­
ном примере приложение уже вывело на экран страницу каталога товаров и
пользователь только что щелкнул на кнопке Добавить в корзину, расположенной
рядом с одним из товаров. Щелчок на этой кнопке приводит к отправке сообще­
ния по адресу http://iocalhost:3000/!ine_item s?productjd=2, где l i n e _ i t e m s — это
ресурс в нашем приложении, а 2 — это внутренний идентификатор выбранного
товара.

О http://my.url/lineJtems?product_id=2
Маршрутизатор
ф Маршрутизатор находит контроллер
каталога товаров Lineltems
'3> Контроллер взаимодействует с моделью

I Контроллер
каталога
товаров
а Контроллер вызывает представление
(Ь) Представление отображает следующий
экран браузера

Представление
каталога
товаров

Рис. 3.2. Rails и MVC

Компонент, занимающийся маршрутизацией, получает входящий запрос и тут


же разбивает его на части. В запросе содержится путь (/lineJtem s?product_id=2)
и метод (данная кнопка инициирует операцию POST; другими обычными методами
являются GET, PUT и DELETE). В нашем простейшем примере Rails рассматрива­
ет первую часть пути, linejtem s, в качестве имени контроллера, a p roductjd — в ка­
честве идентификатора товара. По соглашению POST-методы связаны с действием
c r e a t e ( ). В результате этого анализа маршрутизатор знает, что нужно вызвать
Глава 3 • Архитектура Rails-приложений 51

метод c r e a t e ( ) в классе контроллера L i n e l t e m s C o n t r o l l e r (соглашение об


именах будет рассмотрено в разделе 18.2).
Метод c r e a t e () занимается обработкой пользовательских запросов. В данном
случае он ищет корзину покупок текущего пользователя (которая является объек­
том, управляемым моделью). Он также обращается к модели за поиском информа­
ции для товара 2. Затем он заставляет корзину добавить в нее этот товар. (Видите,
как модель используется для отслеживания всех бизнес-данных? Контроллер
говорит ей, что делать, а модель знает, как это сделать.)
Теперь, когда в корзине новый товар, ее можно показать пользователю. Контрол­
лер вызывает код представления, но перед этим он делает так, чтобы представле­
ние имело доступ к объекту корзины c a r t из модели. В Rails этот вызов зачастую
просто подразумевается, здесь опять соглашения помогают связать конкретное
представление с данным действием.
Это все, что можно сказать в отношении веб-приложения, использующего ар­
хитектуру MVC. Следуя набору соглашений и соответствующим образом разгра­
ничивая выполняемые функции, вы увидите, что с вашим кодом становится проще
работать, а ваше приложение легче поддается расширению и поддержке. Все это
напоминает хорошо организованную коммерческую деятельность.
Если концепция MVC сводится всего лишь к вопросу конкретного способа
разделения вашего программного кода, зачем тогда нужна такая среда, как Ruby
on Rails? Ответ очевиден: Rails берет на себя всю низкоуровневую работу, всю
эту мелочную возню, которая отнимает у вас так много времени, и позволяет вам
сконцентрироваться на основных функциях вашего приложения. Посмотрим, как
это делается.

3.2. Поддержка модели Rails


Как правило, от нашего веб-нриложения требуется, чтобы его информация хра­
нилась в реляционной базе данных. В системах учета поступающих заказов все
заказы, товарные позиции и данные клиентов хранятся в таблицах базы данных.
Даже такие приложения, которые обычно работают с неструктурированным тек­
стом, например блоги и сайты новостей, зачастую используют базы данных в своих
серверных хранилищах.
Хотя из названия языка SQL1, который используется для доступа к базам дан­
ных, этого сразу и не понять, но все реляционные базы данных фактически построе­
ны на основе математической теории множеств. Может быть, концептуально это
и неплохо, но данное обстоятельство сильно затрудняет сочетаемость реляционных
баз данных с объектно-ориентированными языками программирования. Объ­
екты описываются данными и операциями, а базы данных — наборами значений.
Операции, которые легко выражаются в реляционных понятиях, иногда довольно

1 SQL, которые некоторые называют структурированным языком запросов — Struc­


tured Query Language, является языком, используемым для запросов к реляционным
базам данных и для обновления их содержимого.
52 Часть I • Начало

трудно поддаются программированию в объектно-ориентированных системах.


Справедливо также и обратное утверждение.
Со временем специалисты разработали ряд способов согласования реляционных
и объектно-ориентированных взглядов на свои корпоративные данные. Давайте
посмотрим на тот способ отображения реляционных данных на объекты, который
был выбран в Rails.

Объектно-реляционное отображение
Отображением таблиц баз данных на классы занимаются библиотеки O R M . Если
в базе данных есть таблица под названием o r d e r s (заказы), у нашей програм­
мы будет класс по имени O r d e r . Сроки в этой таблице соответствуют объектам
класса — конкретный заказ представлен объектом класса O r d e r. Для получения
и установки значений отдельных столбцов используются свойства этого объекта.
У нашего объекта O r d e r есть методы получения и установки количества товара,
размера налога с продаж и т. д.
Кроме этого Rails-классы, поглотившие таблицы нашей базы данных, предо­
ставляют набор методов на уровне класса, которые выполняют операции на уровне
всей таблицы. Например, может потребоваться найти заказ с конкретным иденти­
фикатором. Этот поиск реализуется в виде метода класса, возвращающего соот­
ветствующий объект O r d e r. В коде Ruby это может иметь следующий вид:
o rd er = O rd e r.fin d (l)
puts "Клиент # {o rd e r .c u s to m e r_ id }, ко л и чество = $ # {о ^ ег. amount}"

Иногда такие методы на уровне класса возвращают коллекцию объектов:


Order.w here(nam e: 'd a v e ').e a c h do |order|
puts order.am ount

И, наконец, объекты, соответствующие отдельным строкам таблицы, имеют ме­


тоды, работающие со строкой. Наверное, наиболее широко используемым является
метод s a v e ( ), выполняющий операцию сохранения строки в базе данных:
O rder.w here(nam e: 'd a v e ').e a c h do |order|
o rd e r.p a y _ ty p e = "Зака з товара"
o rd e r.s a v e

Итак, на уровне ORM таблицы отображаются на классы, строки —на объекты,


а столбцы — на свойства этих объектов. Методы класса используются для выпол­
нения операций на уровне таблиц, а методы экземпляров выполняют операции над
отдельными строками.
Для определения порядка отображения между примитивами базы данных и при­
митивами программы обычной ORM-библиотеке предоставляются конфигураци­
онные данные. Программисты, использующие такие ORM-инструменты, знают, что
им придется создавать и поддерживать полный пакет конфигурационных файлов
в формате XML.
Глава 3 • Архитектура Rails-приложений 53

Active Record
Active Record — это ORM -вставка, предоставляемая Rails. Она строго следует
стандартам ORM-модели: таблицы отображаются на классы, строки —на объекты,
а столбцы — на свойства объекта. От большинства других ORM-библиотек она от­
личается способом конфигурирования. Основываясь на соглашении и приступая
к работе с оптимальными настройками по умолчанию, Active Record сводит к ми­
нимуму объем настроек, выполняемых разработчиками.
Чтобы проиллюстрировать эту особенность, рассмотрим программу, исполь­
зующую Active Record для поглощения нашей таблицы заказов:
re q u ire ' a c t iv e _ r e c o r d '

c la s s Order < A c tiv e R e c o rd : : Base

o rd er = O rd e r.fin d (l)
o rd e r.p a y _ ty p e = "Зака з товара"
o rd e r.s a v e

В этом коде для извлечения ордера с идентификатором, равным 1, и изменения


свойства p a y _ t y p e используется новый класс O rd e r. (Здесь не показан код, соз­
дающий соответствующее подключение к базе данных.) Active Record избавляет
нас от возни с исходной базой данных, позволяя спокойно работать над бизнес-
логикой.
Но Active Record занимается не только этим. В главе 5, при разработке прило­
жения, обслуживающего корзину покупок, будет показано, что Active Record легко
интегрируется со всей остальной средой Rails. Если веб-форма отправляет данные
приложения, связанные с бизнес-объектом, Active Record может извлечь их в нашу
модель. Active Record поддерживает современную проверку приемлемости данных
модели, и, если данные формы не проходят проверку, представление Rails может
извлечь и отформатировать ошибки.
Active Record является надежным фундаментом модели MVC-архитектуры,
используемой в Rails.

3.3. Action Pack: представление


и контроллер
Если призадуматься, части MVC — представление и контроллер — тесно связаны
друг с другом. Контроллер снабжает представление данными, и он же восприни­
мает события от страниц, сгенерированных представлениями. Из-за такого тесного
взаимодействия поддержка представлений и контроллеров в Rails объединена
в единый компонент —Action Pack.
Но не стоит думать, что в вашем приложении код представления и код кон­
троллера будут перемешаны только потому, что Action Pack является единым
54 Часть I • Начало

компонентом. Все как раз наоборот, Rails предоставляет вам разделение, необ­
ходимое для создания приложений с четко разграниченным кодом для логики
управления и представления.

Поддержка представления
Представление в Rails отвечает за создание полного или частичного ответа,
отображаемого в браузере, обработанного приложением или посланного в виде
электронной почты. В простейшем виде представление является фрагментом
HTML-кода, отображающего какой-нибудь неизменный текст. Но чаще всего вам
потребуется включить динамическое содержимое, созданное методом действия
в контроллере.
Динамическое содержимое в Rails генерируется шаблонами трех видов. В са­
мой распространенной схеме создания шаблонов, которая называется встроенным
Ruby —Embedded Ruby (ERb), фрагменты кода Ruby вставляются в представляе­
мый документ, что во многом похоже на способ, применяемый в других веб-средах,
например в РН Р или в JSP. При всей гибкости данного подхода, некоторые спе­
циалисты озабочены тем, что он нарушает сам смысл МУС. Вставляя код в пред­
ставление, мы рискуем заложить в него логику, которая должна быть в модели или
в контроллере. Как и во всем остальном, разумная умеренность будет полезной,
а злоупотребление может превратиться в серьезную проблему. Соблюдение четкого
разделения интересов является частью труда разработчика. (HTML-шаблоны будут
рассмотрены в разделе 25.2 «Генерация HTML с ERb».)
ERb можно также использовать для конструирования на сервере JavaScript-
фрагментов, выполняемых в браузере. Эта технология отлично подходит для
создания динамичных AJAX-интерфейсов. К этому вопросу мы еще вернемся
в разделе 11.2.
В Rails также имеется такой инструмент, как XML Builder, позволяющий кон­
струировать XML-документы, использующие код Ruby, —структура генерируемого
XML будет автоматически следовать за структурой кода. Шаблоны x m l . b u i l d e r
будут рассмотрены, начиная с раздела 25.1.

И наконец, контроллер!
Контроллер Rails является логическим центром вашего приложения. Он коорди­
нирует взаимодействие между пользователем, представлениями и моделью. Но с
большинством этих взаимодействий Rails справляется без вашего участия, а со­
здаваемый вами код концентрируется на функциональности прикладного уровня.
Это существенно упрощает разработку и поддержку кода Rails-контроллера.
Контроллер также является местом для нескольких важных вспомогательных
служб:
О Он отвечает за перенаправление внешних запросов внутренним действиям.
Он отлично справляется с удобными для человеческого восприятия URL-
адресами.
Глава 3 • Архитектура Rails-приложений 55

О Он управляет кэшированием, ускоряющим работу приложения в несколько


раз.
О Он управляет вспомогательными модулями, расширяющими возможности
шаблонов представлений без увеличения объема их кода.
О Он управляет сессиями, дающими пользователям ощущение непрерывного
взаимодействия с нашими приложениями.
Мы уже работали с контроллером в разделе 2.2, а при разработке учебного при­
ложения нам еще придется поработать с несколькими контроллерами, начиная
с контроллера товаров в разделе 8.1 «Шаг В1: создание каталога товаров».
О Rails еще многое можно сказать. Но, перед тем как продолжить, давайте не­
много освежим свои знания Ruby, а для некоторых из вас предоставим краткое
введение в этот язык.
Введение в Ruby

О сн овн ы е тем ы :

> объекты : имена и методы;


> данные: строки, массивы , хэш и и регулярны е вы раж ения;
> элем енты управления: if, w hile, блоки, итераторы и исклю чения;
> строительны е блоки: классы и модули;
> YAM L и м арш ализация;
> общ ие идиомы , которы е вы уви ди те в данной книге.

Для многих, кто только начинает знакомиться с Rails, также в новинку и Ruby. При
знакомстве с такими языками, как Java, JavaScript, РНР, Perl или Python, освоить
Ruby будет нетрудно.
Эта глава не является полноценным введением в Ruby. В ней не рассматрива­
ются такие темы, как порядок выполнения действий (как и в большинстве других
языков программирования, в Ruby 1+2*3==7). Она предназначена для объяснения
Ruby только в том объеме, который позволяет сделать приводимые в книге при­
меры более понятными.
Эта глава во многом повторяет материал главы 2 книги «Programming Ruby»1.
Если у вас есть потребность в более основательном изучении языка Ruby, то мы,
рискуя быть обвиненными в преследовании личных интересов, все же хотели бы
предложить лучший способ изучения Ruby и лучший справочник по классам, мо­
дулям и библиотекам этого языка —книгу «Programming Ruby» (известную также
как «PickAxe»). Добро пожаловать в сообщество Ruby!

' David Thomas, Chad Fowler, and Andrew Hunt. Programming Ruby: The Pragmatic
Programmer’s Guide. The Pragmatic Bookshelf, Raleigh, NC and Dallas, TX, Third,
2008.
Глава 4 • Введение в Ruby 57

4.1. Ruby — объектно-


ориентированный язык
Все, с чем вы работаете в Ruby, является объектом, а результаты этой работы также
являются объектами.
При написании объектно-ориентированного кода обычно обращаются к по­
нятиям модели из реального мира. Как правило, в ходе этого процесса модели­
рования изучаются категории тех вещей, которые должны быть представлены.
В интернет-магазине такой категорией может стать понятие отдельной товарной
позиции. Для представления каждой из таких категорий в Rubv следует определить
отдельный класс. Затем этот класс используется в качестве фабрики, создающей
объекты —экземпляры этого класса. Объект является комбинацией состояния
(например, количество и идентификатор товара) и методов, использующих это со­
стояние (возможно, метода для вычисления общей стоимости товарной позиции).
Создание классов будет показано в разделе 4.4.
Объекты создаются путем вызова конструктора, который является специальным
методом, связанным с классом. Стандартный конструктор называется new().
При наличии класса под названием L in e lt e m , объекты товарных позиций мож­
но создавать следующим образом:
lin e _ ite m _ o n e = Lineltem .new
lin e _ ite m _ o n e .q u a n tity = 1
lin e _ ite m _ o n e . sku = "AUTO_B_00"

Методы вызываются путем отправки объекту сообщения. Эго сообщение со­


держит имя метода наряду с любыми необходимыми методу аргументами. Когда
объект получает сообщение, он ищет соответствующий метод внутри своего соб­
ственного класса. Рассмотрим несколько вызовов метода:
"d a v e ". len g th
lin e _ ite m _ o n e . q u a n t it y ( )
c a r t . a d d _ lin e _ ite m (n e x t_p u rc h a s e )
subm it_tag "Добавить в корзину"

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


В приложениях Rails можно заметить, что в длинных выражениях в большинстве
вызовов метода круглые скобки используются, а там, где эти вызовы больше по­
хожи на команду или объявление, скобки обычно не ставятся.
У методов, как и у многих других конструкций Ruby, имеются имена, которые
подчиняются определенным правилам. Если вы занялись Rub}' после работы с дру­
гими языками, возможно, эти правила вам еще не попадались.

Имена, используемые в Ruby


Все имена локальных переменных, аргументов методов и самих методов должны
начинаться с буквы в нижнем регистре или со знака подчеркивания: o r d e r , l i n e _
it e m и x r 2000 являются допустимыми именами. Имена переменных экземпляра
58 Часть I • Начало

(которые рассматриваются в разделе 4.4) начинаются с символа «at» (@), например,


^ q u a n t i t y и @ p r o d u c t _ id . По принятому в Ruby соглашению знак подчеркива­
ния используется для разделения слов в составном имени метода или переменной
(то есть l i n e _ i t e m использовать предпочтительнее, чем l i n e l t e m ) .
Имена классов модулей и констант должны начинаться с буквы в верхнем ре­
гистре. По соглашению для выделения начала слов внутри их имен используются
не знаки подчеркивания, а заглавные буквы. Имена классов имеют вид O b j e c t ,
P u r c h a s e O r d e r и L in e lt e m .
В целях идентификации в Ruby используются обозначения. В частности, они
используются в качестве ключей при названии аргументов методов и при поисках
в хэшах. Например:
r e d ir e c t _ t o a c tio n : "e d it" , id : p a ra m s [:id ]

Как видите, обозначения похожи на имена переменных, только в качестве пре­


фикса в них используется двоеточие. В качестве примеров обозначений можно
привести t a c t i o n , : l i n e _ i t e m s и : id . Обозначения можно считать строковыми
литералами, магическим образом превращающимися в константы. Смысл двое­
точия также можно рассматривать как «что-то по имени...», соответственно, : i d
обозначает «что-то но имени id».
После того как мы уже использовали несколько методов, давайте перейдем
к вопросу их определения.

Методы
Давайте напишем метод, возвращающий теплое персональное пожелание. А затем
мы этот метод пару раз вызовем:
d ef say_goodnight(nam e)
r e s u lt = 'Спокойной ночи, ' + name
re tu rn r e s u lt

# Пора с п а т ь . . .
puts s a y _ g o o d n ig h t(’ Мэри-Эллен' )
puts s a y _ g o o d n ig h t(' крошка Джон')

После определения мы дважды вызываем этот метод. В обоих случаях результат


передается методу p u t s ( ), который выводит на консоль свои аргументы, завершая
вывод символом новой строки (перемещением на новую строку вывода).
При помещении каждой инструкции в отдельной строке точка с запятой в конце
инструкции не требуется. Комментарии в Ruby начинаются с символа # и продол­
жаются до конца строки. Отступы не играют никакой роли (но де-факто отступ
в две символьных позиции является в Ruby стандартом).
Для ограничения тел составных инструкций и определений (например, методов
и классов) фигурные скобки в Rubv не используются. Вместо этого тело просто за­
вершается ключевым словом end. Ключевое слово r e t u r n является необязательным,
и, если оно отсутствует, возвращается результат вычисления последнего выражения.
Глава 4 • Введение в Ruby 59

4.2. Типы данных


Наряду с тем, что все в Ruby является объектом, некоторые типы данных имеют
специальную синтаксическую поддержку, в частности, для определения лите­
ральных значений. В данных примерах использован ряд простых строк и даже
конкатенация строк.

Строки
В предыдущем примере был, кроме всего прочего, показан ряд строковых объектов.
Один из способов создания строкового объекта заключается в использовании стро­
ковых литералов, представляющих собой последовательности символов внутри
одинарных или двойных кавычек. Разница между двумя формами состоит в объеме
той обработки, которой подвергается строка со стороны Ruby при создании лите­
рала. В случае использования одинарных кавычек Ruby практически ничего не
делает. За некоторыми исключениями все, что набрано в строковом литерале, за­
ключенном в одинарные кавычки, становится значением строки.
В случае использования двойных кавычек Ruby проводит более серьезную
работу. Во-первых, он ищет подстановки — последовательности, начинающиеся
с символа обратного слэша, — и заменяет их неким двоичным значением. Самой
распространенной подстановкой является пара символов \п, которая заменяется
кодом символа новой строки. При записи строки, содержащей эту подстановку на
консоль, \п приводит к переносу строки.
Во-вторых, в строках, заключенных в двойные кавычки, Ruby выполняет встав­
ку результата вычисления выражения. Имеющаяся в строке последовательность
#{ выражение } заменяется значением выражения. Этим можно воспользоваться,
чтобы переписать наш предыдущий метод:
def say_goodnight(nam e)
"Спокойной ночи, #{name. c a p i t a l i z e } "
end
puts s a y _ g o o d n ig h t(' p a ' )

Когда Ruby создает этот строковый объект, он смотрит на текущее значение name
и подставляет его в строку. В конструкции #{...} допускаются выражения произволь­
ной сложности. В данном случае вызывается метод c a p i t a l i z e ( ), определенный для
всех строк, чтобы выдать наш аргумент с первой буквой в верхнем регистре.
Строки являются довольно простым типом данных, содержащим упорядочен­
ную совокупность байтов или символов. Ruby также предоставляет средства для
определения коллекций произвольных объектов с помощью массивов и хэшей.

Массивы и хэши
Массивы и хэши в Ruby являются индексированными коллекциями. В них хранят­
ся совокупности объектов, доступных при использовании ключа. В массивах ключ
является целым числом, а кэши в качестве ключа поддерживают любой объект.
60 Часть I • Начало

Массивы и хэши разрастаются по мере необходимости содержания новых элемен­


тов. Доступ к элементам массива более эффективен, но хэши предоставляют более
высокую гибкость. Каждый конкретный массив или хэш может содержать объекты
разных типов: к примеру, можно создать массив, содержащий целые числа, строки
и числа с плавающей точкой.
Создать и инициализировать новый объект массива можно с помощью лите­
рала массива — набора элементов между квадратными скобками. Как показано
в следующем примере, получение объекта массива дает возможность обратиться
к отдельным элементам путем предоставления индекса, заключенного в квадратные
скобки. В Ruby индексация массивов начинается с нуля.
а = [ 1, ' c a t ' , 3.14 ] # массив из трех элементов
а [0 ] # обращение к первому элементу (1 )
а [2 ] = n i l # установка значения третьего элемента
# теперь массив имеет следующий вид: [ 1, 'c a t ' , n il ]

Можно было заметить, что в этом примере использовалось специальное значе­


ние n il. Во многих языках понятие nil (или null) означает «объект отсутствует».
Но в Ruby все по-другому: n i l является таким же объектом, как и все остальные,
и предназначен для представления отсутствия.
С массивами часто используется метод << (). Он добавляет значение к своему
получателю:
ages = [ ]
f o r person in @people
ages << person.age

Для создания массива слов в Ruby есть сокращенная форма записи:


а = [ 'муравей' , 'п че л а ' , 'к о т ' , 'с о б а к а ' , 'л о с ь ' ]
# эта запись аналогична предыдущей:
а = %w{ муравей пчела кот собака лось }

Хэши в Ruby похожи на массивы. В хэш-литералах вместо квадратных скобок


используются фигурные. Каждая запись литерала должна быть представлена двумя
объектами: одним — в качестве ключа, и другим —в качестве значения. Например,
может потребоваться отобразить музыкальные инструменты на их принадлежность
к оркестровым группам.
in s t _ s e c t io n = {
: виолончель => 'струнные инструменты' ,
: кларнет => 'деревянные духовые инструменты' ,
: барабан => 'ударные инструменты' ,
: гобой => 'деревянные духовые инструменты' ,
: труба => 'медные духовые инструменты' ,
: скрипка => 'струнные инструменты'
}
Все, что находится левее группы символов =>, является ключом, а все, что
справа, —соответствующим ему значением. Ключи в каждом хэше должны быть
Глава 4 • Введение в Ruby 61

уникальны —не может быть двух записей с ключом : барабан. В качестве ключей
и значений хэша могут использоваться объекты любого типа — могут быть хэши,
значения которых представлены массивами, другими хэшами и т. д. Как правило,
в Rails в качестве ключей для хэшей используются обозначения. Многие хэши в
Rails подверглись незначительной модификации, поэтому в качестве ключей при
вставке и просмотре значений в равной степени могут использоваться как строки,
так и обозначения.
Использование обозначений в качестве ключей хэшей стало настолько при­
вычным, что, начиная с Ruby 1.9, для этого используется специальный синтаксис,
более простой для набора и более наглядный:
in s t _ s e c t io n = {
виолончель: 'струнные инструменты' ,
кларнет: 'деревянные духовые инструменты' ,
барабан: 'ударные инструменты' ,
гобой: 'деревянные духовые инструменты' ,
труба: 'медные духовые инструменты’ ,
скрипка: 'струнные инструменты'
>
Не правда ли, смотрится намного лучше?
Можно использовать любой понравившийся вам синтаксис. Можно даже сме­
шивать в одном выражении разные варианты. Разумеется, если в качестве ключей
используются не обозначения или если используется Ruby 1.8.7, следует придер­
живаться синтаксиса, применяющего «стрелки». Но похоже, что многие разработ­
чики отдают предпочтение новому синтаксису, и Rails будет даже генерировать
временные платформы (scaffolds), используя новый синтаксис, если обнаружит,
что у вас запущен Rails 1.9.2.
При индексном доступе к элементам хэша используются те же квадратные
скобки, что и в массивах:
i n s t _ s e c t i o n [ : гобой] #=> 'деревянные духовые инструменты'
i n s t _ s e c t i o n [ : виолончель] #=> 'струнные инструменты'
i n s t _ s e c t i o n [ :фагот] #=> n i l

Как показано в предыдущем примере, при индексном доступе с ключом, не со­


держащимся в хэше, возвращается n i l . Такое свойство представляется удобным,
поскольку при использовании в условных выражениях n i l означает f a l s e .
Хэши можно передавать в качестве аргументов при вызове методов. Ruby по­
зволяет не ставить скобки, но только в том случае, когда в списке аргументов вы­
зова хэш идет последним. В Rails эта возможность используется довольно широко.
В следующем фрагменте кода показан двухэлементный хэш, передаваемый методу
r e d i r e c t _ t o . О том, что это хэш, можно просто забыть, и представить, что в Ruby
есть именованные аргументы:
r e d ir e c t _ t o :a c t io n => 'show' , :id => p ro d u c t.id

Есть еще один тип данных, вполне застуживающий внимания, —это регулярные
выражения.
62 Часть I • Начало

Регулярные выражения
Регулярные выражения позволяют указать шаблон символов для его сравнения
со строкой. В Rubv регулярное выражение обычно создается в формате / шаблон/
или %г {шаблон}.
Например, используя выражение / P e r l | Python/, можно создать шаблон, соот­
ветствующий строке, в которой содержится текст «Perl» или текст «Python».
Шаблон ограничивается знаками прямых слэшей и содержит два сравниваемых
фрагмента, разделенные вертикальной линией ( | ). Символ вертикальной черты
означает «либо то, что слева, либо то, что справа», в данном случае — либо Perl,
либо Python. В шаблонах, как и в арифметических выражениях, можно исполь­
зовать скобки, поэтому данный шаблон можно написать следующим образом:
/Р(ег11 y th o n )/. В программах для сравнения строк с регулярными выражениями
используется оператор сравнения =~:
if li n e =~ / Р (е г 1 |yth o n )/
puts “ Похоже, здесь используется другой язык сценариев"

В шаблонах можно задавать повторения. Выражение / a b + с / соответствует


строке, содержащей символ а, за которым следует один или несколько символов
Ь, за которыми, в свою очередь, следует символ с. Если заменить знак «плюс»
звездочкой, получится регулярное выражение /а Ь * с /, которое соответствует
одному символу а, нулевому или большему количеству символов b и одному
символу с.
Специальные последовательности начинаются с обратного слэша; наиболее за­
метна последовательность \d, соответствующая одной цифре, последовательность
\s , соответствующая любому пробельному символу, и последовательность \w, со­
ответствующая любым буквенно-цифровым («словарным») символам.
Регулярные выражения Ruby — довольно глубокая и сложная тема, которая
в данном разделе рассмотрена весьма поверхностно. Полноценную информацию
о них можно получить в книге «PickAxe», а в данной книге регулярные выражения
будут применяться лишь эпизодически.
После краткого представления данных, давайте перейдем к логике.

4.3. Логика
Вызовы методов являются инструкциями. В Ruby также предоставляется ряд спо­
собов принятия решения, влияющих на повторения и на тот порядок, в котором
вызываются методы.

Управляющие структуры
В Ruby имеются все стандартные управляющие структуры, например инструкции
i f и циклы w hile. Программисты, работавшие на Java, С и Perl, могут заметить,
Глава 4 • Введение в Ruby 63

что вокруг тел этих операторов отсутствуют фигурные скобки. Вместо них для
обозначения окончания тела в Ruby используется ключевое слово end:
if count > 10
puts "Попробуйте еще раз"
e l s i f t r i e s == 3
puts "Вы проиграли"

puts "Введите число"

Операторы w h ile также заканчиваются ключевым словом end:


w h ile w eight < 100 and n u m _p allets <= 30
p a l le t = n e x t _ p a lle t ( )
w eight += p a lle t .w e ig h t
n u m _p allets += 1

В Ruby имеются также варианты этих инструкций: u n le ss похожа на i f , но она


проверяет условие на недостоверность. Аналогичным образом инструкция u n t i l
похожа на инструкцию w hile, за исключением того, что цикл продолжается до тех
пор, пока вычисляемое условие не вернет tru e .
Если тело инструкций i f или w h ile укладывается в одно выражение, в Ruby
может использоваться удобное сокращение, называемое модификатором инструк­
ции. Нужно просто написать выражение, сопровождаемое ключевым словом мо­
дификатора и условием:
puts "Уилл Робинсон., Вас подстерегает опасность" i f r a d ia t io n > 3000
d is ta n c e = d is ta n c e * 1.2 w h ile d is ta n c e < 100

Инструкции i f встречаются в Ruby-приложениях довольно часто, но тех, кто


начинает осваивать язык Ruby, зачастую удивляет редкое использование конструк­
ций циклов. Вместо них часто используются блоки и итераторы.

Блоки и итераторы
Блоки кода —это фрагменты программного кода, заключенные в фигурные скобки
или помещенные между ключевыми словами d o . . , end. По общепринятому согла­
шению, фигурные скобки используются для однострочных блоков, а конструкция
do/end —для многострочных:
{ puts "Привет" } # это блок

do ###
c lu b .e n r o ll(p e r s o n ) # и это тоже блок
p e rso n . s o c ia liz e #
end ###

Чтобы передать блок методу, его нужно поместить после аргументов метода
(если таковые имеются). Иными словами, начало блока нужно поместить в конец
64 Часть I • Начало

исходной строки, содержащей вызов метода. Например, в следующем коде блок,


содержащий p u ts "П ри вет", связан с вызовом метода g r e e t Q :
g re e t { puts "Привет" }

Если у вызова метода имеются аргументы, они ставятся перед блоком:


v e r b o s e _ g re e t("Д эй в", "постоянный кли ен т") { puts "Привет" }

Используя Ruby-инструкцию y i e l d , метод может вызывать связанный с ним


блок один или несколько раз. Инструкцию y i e l d можно считать своеобразным
вызовом метода, который вызывает блок, связанный с тем методом, в котором
содержится y i e l d . Предоставляя аргументы инструкции y i e l d , блоку можно
передавать значения. Внутри блока перечень имен аргументов, получающих эти
значения, помещается между вертикальными линиями ( | ).
Блоки кода встречаются в Ruby-приложениях повсеместно. Зачастую они ис­
пользуются вместе с итераторами —методами, которые возвращают последователь­
ные элементы из какой-нибудь коллекции, например из массива:
anim als = %w{ муравей пчела кот собака лось } # создание массива
an im a ls.e ach { | a n im a l| puts anim al } # последовательный перебор содержимого

Каждое целое число N вызывает метод t i m e s Q , который, в свою очередь, вы­


зывает связанный с ним блок N раз:
3 .tim es { p r in t "Эй! " } #=> Эй! Эй! Эй!

Префиксный оператор &позволит методу захватить переданный блок в качестве


поименованного аргумента:
d ef wrap &b
p r in t "Санта сказа л: "
3 .tim e s (& b )
p r in t "\n "
end
wrap { p r in t "Эй! " }

Внутри блока или метода управление инструкциям передается последовательно,


если только там не будет исключения.

Исключения
Исключения являются объектами (класса E x c e p tio n или его подклассов). Для
выдачи исключения используется метод r a is e . Тем самым прерывается нормаль­
ная последовательность выполнения программного кода, и Ruby просматривает
в обратном порядке стек вызовов в поиске кода, который сообщит о возможности
обработки этого исключения.
Соответствующие классы исключений перехватываются с помощью выраже­
ний re sc u e как методами, так и блоками кода, помещенными между ключевыми
словами begin и end.
Глава 4 • Введение в Ruby 65

begin
co n ten t = lo a d _b lo g _d ata(file_n am e)
rescue BlogDataNotFound
STDERR. puts "Файл #{file_nam e} не найден"
rescue Blog D ataForm atErro r
STDERR.puts "В файле #{file_nam e} отсутствую т данные блога"
rescue Ex cep tio n => exc
STDERR.puts " Общая ошибка загрузки # {file _n a m e }: # {e x c.m essa g e }"
end

Выражения rescue могут непосредственно помещаться на самом внешнем уров­


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

4.4. Организационные структуры


Для организации методов в Ruby есть два основных понятия: классы и модули. Мы
рассмотрим по очереди каждое из них.

Классы
Определение класса имеет в Ruby следующий вид:
Строка
1 c la s s Order < A c tiv e R e c o rd : : Base
has_many :lin e _ ite m s

d e f s e l f ,fin d _a ll_u n p a id
5 s e l f . w h e r e ( ' paid = 0 ' )
end

def to t a l
sum = 0
10li n e _ it e m s . each { | l i | sum += l i . t o t a l }
sum
end
end

Это определение начинается с ключевого слова c l a s s , за которым следует имя


класса (которое должно начинаться с буквы в верхнем разряде). Данный класс
O r d e r определен как подкласс, относящийся к классу Base, который находится
в модуле A c t i v e R e c o r d .
Rails довольно интенсивно привлекается к определениям на уровне классов.
В данном случае has_many — это метод, который определен в Active Record. Он
вызывается при определении класса Order. Обычно методы такого рода позволяют
судить о классе, поэтому в данной книге мы называем их объявлениями.
66 Часть I • Начало

Внутри тела класса можно определить методы класса и методы экземпляра.


Добавление к имени метода префикса s e l f . (как это сделано в строке 4) делает его
методом класса: он может быть вызван во всем классе. В данном случае можно из
любого места нашего приложения сделать следующий вызов:
t o _ c o l le c t = O rd er.fin d _a ll_u n p a id

Объекты класса хранят свое состояние в переменных экземпляра. Эти пере­


менные, имена которых всегда начинаются с символа §, доступны всем методам
экземпляра определенного класса. Каждый объект получает свой собственный
набор переменных экземпляра.
Непосредственный доступ к переменным экземпляра за пределами класса
невозможен. Для доступа к ним нужно создавать методы, возвращающие их
значения:
c la s s G re e te r
d e f in it ia liz e ( n a m e )
@name = name
end

d e f name
(Sname
end

def name=(new_name)
(Sname = new_name
end

g = G r e e t e r .п еш ("Бзрни")
puts g.name #=> Барни
g.name = "Б е тти "
puts g.name #=> Бетти

Для создания этих методов доступа Ruby предоставляет свои, очень удобные
методы (это должно понравиться тем, кто уже устал от написания всех этих из-
влекателей и установщиков значений свойств):
c la s s G re e te r
a t tr _ a c c e s s o r :name # создание методов чтения и записи
a t t r _ r e a d e r : g re e tin g # создание только метода чтения
a t t r _ w r i t e r :age # создание только метода записи

По умолчанию методы экземпляра класса являются открытыми ( p u b l i c ) и мо­


гут быть вызваны отовсюду. Возможно, вам потребуется отменить эту установку
для тех методов, которые предназначены для использования только другими ме­
тодами экземпляра класса:
c la s s MyClass
d e f ml # Это открытый метод
end
Глава 4 • Введение в Ruby 67

p ro te cte d

d e f m2 # Это защищенный метод


end

p r iv a te

d e f m3 # это закрытый метод


end

Самой строгой является директива p r iv a t e ; закрытые методы могут быть вы­


званы только внутри того же самого экземпляра. Защищенные методы могут быть
вызваны как из того же самого экземпляра, так и из других экземпляров того же
класса и его подклассов.
Классы не являются в Ruby единственной организационной структурой. Еще
одной такой структурой является модуль.

Модули
Модули похожи на классы тем, что они содержат коллекцию методов, констант,
а также определений других модулей и классов. В отличие от классов, создавать
объекты на основе модулей невозможно.
Модули служат двум целям. Во-первых, они работают как пространства имен,
позволяя определять методы, чьи имена не будут конфликтовать с именами ме­
тодов, определенных в каком-нибудь другом месте. Во-вторых, они позволяют
иметь для классов общие функциональные возможности — если класс смешива­
ется с модулем, методы экземпляра модуля становятся доступны, как будто они
были определены в самом классе. С одним и тем же модулем могут смешиваться
несколько классов, вместе используя функциональные возможности модуля без
привлечения механизма наследования. Можно также смешать с отдельным классом
сразу несколько модулей.
Примером использования модулей в Rails могут послужить вспомогательные
методы. Rails автоматически смешивает вспомогательные модули с соответствую­
щими шаблонами представления. Например, если нужно написать вспомогатель­
ный метод, который может быть вызван из представления, вызванного контролле­
ром s t o r e , можно в файле store_helper.rb каталога app/helpers определить следующий
модуль:
module Sto re H e lp e r
d ef c a p it a liz e _ w o r d s (s t r in g )
s t r i n g . s p l i t ( ' ').m ap {|w o rd | word. c a p i t a l i z e } . j o i n ( ' ')
end

Модуль YAML, входящий в стандартную библиотеку Ruby, заслуживает особого


упоминания, с учетом его использования в Rails.
68 Часть I • Начало

YAML
YAML1 — это рекурсивный акроним, означающий YAML Ain’t Markup Language
(YAML — не язык разметки). Применительно к Rails YAML используется как
удобный способ определения конфигураций баз данных, тестовых данных и пре­
образований. Например:
development:
a d a p te r: s q lit e 3
database: d b /d evelo p m en t.sq lite3
pool: 5
tim e o u t: 5000

В YAML важную роль играют отступы, поэтому здесь определяется, что


developm ent (разработка) имеет набор из четырех пар ключ-значение, с двоето­
чием в качестве разделителя.
Хотя YAML является одним из способов представления данных, особенно
в случаях общения с людьми, Ruby предоставляет более универсальный способ
представления данных для их использования в приложениях.

4,5. Маршализированные объекты


Rubv способен конвертировать какой-либо объект в поток байтов, который может
быть сохранен вне приложения. Этот процесс называется маршализацией. Затем
сохраненный объект может быть прочитан другим экземпляром приложения (или
вообще другим приложением), в результате чего может быть восстановлена копия
оригинального объекта.
При использовании маршализации существует две потенциальные проблемы.
Во-первых, некоторые объекты не поддаются разложению в двоичную форму:
если в подвергаемых обработке объектах содержатся какие-нибудь связывания,
объекты процедур или методов, экземпляры класса 10 или синглтон-объекты или
если предпринимается попытка перевести в двоичную форму безымянные классы
или модули, будет выдано исключение Т у р е Е г г о г .
Во-вторых, когда загружаются маршализированные объекты, Ruby нужно знать
определение класса этого объекта (и всех содержащихся в нем объектов).
Rails использует маршализацию для хранения данных сессии. Если полагаться
на Rails при динамической загрузке классов, может получиться, что конкретный
класс на момент восстановления данных сессии не будет определен. Именно поэто­
му в контроллере используется объявление model для перечисления всех моделей,
подвергающихся маршализации. Это объявление приводит к предварительной
загрузке классов, необходимых для работы маршализации.
Ознакомившись с основами Ruby, давайте смешаем все изученное в более объ­
емном, прокомментированном примере, объединившем сразу несколько понятий.

1 http://www.yaml.org/
Глава 4 • Введение в Ruby 69

Затем будет дан краткий обзор характерных особенностей, которые помогут про­
граммированию в среде Rails.

4.6. А теперь все вместе


Рассмотрим пример того, как Rails сводит в единое целое сразу несколько свойств
Ruby, чтобы сделать программный код, нуждающийся в поддержке, более описа­
тельным. Этот пример будет снова показан в главе 6, в разделе «Создание времен­
ных платформ». А сейчас основное внимание будет уделено тем аспектам примера,
которые связаны с языком Ruby.
c la s s C re ate Pro d u cts < A c tiv e R e c o rd ::M ig ra tio n
d e f change
c r e a t e _ ta b le : products do |t|
t.s t r in g : t i t l e
t .t e x t d e s c rip tio n
t . s t r i n g :im ag e_u rl
t.d e c im a l :p r ic e , p r e c is io n : 8, s c a le : 2

t.tim estam p s
end
end

Даже не зная языка Ruby, можно понять, что этот код создает таблицу по имени
p r o d u c t s . При создании этой таблицы определяются поля t i t l e , d e s c r i p t i o n ,
i m a g e _ u r l и p r i c e , а также несколько временных меток, t im e s t a m p s (они будут
рассмотрены в разделе 23.1).
Давайте посмотрим на этот пример с точки зрения Ruby. В нем определяется
класс по имени C r e a t e P r o d u c t s , который наследуется из класса M i g r a t i o n модуля
A c t iv e R e c o r d . Определяется один метод по имени c h a n g e ( ). Этот метод вызывает
единственный метод класса (определенный в A c t i v e R e c o r d : : M i g r a t i o n ) , пере­
давая ему имя таблицы в форме обозначения.
Вызову c r e a t e _ t a b l e ( ) также передается блок, который должен быть вы­
числен до создания таблицы. При вызове этого блока ему передается объект по
имени t , который используется для сбора списка полей. Для этого объекта Rails
определяет ряд методов, которые носят названия стандартных типов данных.
При вызове эти методы просто добавляют определения полей к накапливаемому
набору имен.
Определение d e c i m a l также принимает ряд необязательных параметров, пред­
ставленных хэшем.
Тем, кто не знаком с Ruby, кажется, что для решения такой простой проблемы
привлекается слишком много сложных механизмов, а для тех, кто знаком с этим
языком, ни один из этих механизмов не кажется особенно сложным. В любом случае
Rails широко использует средства, предоставляемые Ruby, для того, чтобы сделать
операции определения (например, миграционных задач) как можно проще и опи-
сательнее. Даже такие незначительные особенности языка, как необязательные
70 Часть I • Начало

круглые и фигурные скобки, используются для достижения повсеместной удобо­


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

4.7. Идиомы, используемые в Ruby


Многие отдельные свойства Ruby могут быть объединены довольно интересным
образом, и значение такого идиоматического использования зачастую не сразу бро­
сается в глаза тем, кто плохо знаком с языком. В данной книге будут использованы
следующие широко распространенные Ruby-идиомы:
Методы типа e m p t y ! и empty?
В Ruby имена методов могут заканчиваться восклицательным знаком (метод-
модификатор) или вопросительным знаком (метод-предикат). Методы-
модификаторы обычно вносят изменения в получатель. Методы-предикаты
в зависимости от определенных условий возвращают t r u e или f a l s e .
a II b
Инструкция а | | b вычисляет а. Если результат не равен f a l s e или n i l ,
вычисление прекращается и инструкция возвращает а. В противном случае
возвращается Ь. Это весьма распространенный способ возвращения значения
по умолчанию, если первое значение не установлено,
а | |= b
Инструкция присваивания поддерживает целый набор сокращений: сокра­
щение вида а оператор= b эквивалентно выражению а = а оператор Ь. Это
сокращение работает с большинством операторов:
count += 1 # эквивалентно count = count + 1
p r ic e *= d isco u nt # p r ic e = p r ic e * d isco u nt
count | | = 0 # count = count | | 0

Таким образом, инструкция c o u n t | | = 0 присваивает переменной c o u n t


значение 0, если у c o u n t еще нет значения,
o b j = s e l f . new
И ногда метод класса должен создать экземпляр этого класса:
c la s s Person < A c tiv e R e c o rd : : Base
d ef s e lf .f o r _ d a v e
P e rso n . new(name: 'Д э й в ')
end
end

Все это прекрасно работает, возвращая новый объект Pe rs o n . Но позже кто-


нибудь может создать подкласс нашего класса:
Глава 4 • Введение в Ruby 71

c la s s Employee < Person


# ..
end
dave = Em ployee. fo r_d a v e # возвращает Person

Метод f o r _ d a v e ( ) был жестко запрограммирован на возвращение объекта


P e r s o n , поэтому вызов E m p l o y e e . f o r _ d a v e возвращает именно его. При
использовании вместо него метода s e l f , new возвращается новый объект
класса-получателя, то есть Employee,
lambda
Оператор lambda превращает блок в объект типа Ргос. Его использование
будет показано в разделе 19.3, в подразделе «Области видимости»,
r e q u ir e F i l e . dirnam e(__FILE__)+ ' / . . / t e s t _ h e l p e r '
Ruby-метод r e q u i r e загружает в приложение файл из внешнего источника.
Это используется для включения кода библиотеки и классов, от которых
зависит работа приложения. При обычном использовании Ruby находит эти
файлы, проводя поиск в каталогах, перечисленных в списке LOAD_PATH.
Иногда нужно точно указать, какой файл следует включить. Это можно сде­
лать, передав методу полное путевое имя файла. Проблема в том, что мы не
знаем, каким будет это путевое имя — наши пользователи могут установить
наш код куда угодно.
В какое бы место ни было в конечном итоге установлено наше приложение,
относительный путь между файлом, из которого делается запрос, и запра­
шиваемым файлом будет одним и тем же. Зная эго, мы можем выстроить
абсолютный путь к запрашиваемому файлу, получив абсолютный путь
к файлу, из которого делается запрос (этот путь доступен в специальной
переменной__ F I L E __ ), убрав из него все, кроме имени каталога, а затем до­
бавив относительный путь к запрашиваемому файлу.
Дополнительно в Интернете имеется множество хороших информационных
ресурсов, в которых показываются используемые в Ruby идиомы и фокусы. Вот
лишь некоторые из них:
О http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/
О http://en.wikipedia.org/wiki/Ruby_programming_ianguage
О http://www.zenspider.com/Languages/Ruby/QuickRef.html
Теперь у нас появился прочный фундамент, на котором можно вести строи­
тельство: мы установили Rails, проверили ее работоспособность на простом при­
ложении, получили некоторое представление о том, что такое Rails, и повторили
(а некоторые впервые изучили) основы языка Ruby. Настало время применить эти
знания при создании более крупного приложения.
Создание приложения
Интернет-магазин

О сн о в н ы е тем ы :

> поэтапная разработка;


> примеры использования, п оследовательность страниц, структура данных;
> приоритеты .

Можно, конечно, потратить весь день на совместный разбор простых тестовых


приложений, но это не поможет нам оплатить счета. Поэтому давайте нацелимся
на что-нибудь более существенное и создадим на основе веб-технологий интернет-
магазин под названием Depot.
Конечно, в мире и гак достаточно всевозможных интернет-магазинов, но сотни
разработчиков продолжают создавать все новые и новые подобные приложения.
А чем мы хуже?
А если серьезно, то наша корзина покупателя поможет проиллюстрировать мно­
гие особенности Rails-разработки. Мы увидим, как создаются простые страницы
обслуживания покупателей, как они привязываются к таблицам базы данных, как
обрабатываются сессии и как создаются формы. На протяжении следующих 12 глав
мы также будем обращаться к внешним темам, таким как блочное тестирование,
безопасность и разметка страницы.

5.1. Поэтапная разработка


Разработка нашего приложения будет вестись поэтапно. Мы не станем пытаться
определить буквально все до начала написания программного кода. Лучше мы
выработаем объем спецификации, позволяющий приступить к программирова­
нию, и тут же создадим нечто работающее. Мы будем проводить практическую
Глава 5 • Интернет-магазин 75

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


мини-проектирования и разработки.
Придерживаться такого стиля программирования удается не всегда. Здесь тре­
буется тесное сотрудничество с пользователями приложения, поскольку по мере
продвижения разработки нам нужно собирать отзывы о работе продукта. Ошибать­
ся можем и мы, и заказчик, обнаруживший, что, требуя от нас одно, он на самом
деле хотел получить совсем другое. Не важно, кто стал виновником ошибки, но
чем раньше мы ее обнаружим, тем меньше сил будет затрачено на ее исправление.
В общем, применение этого стиля разработки характеризуется внесением много­
численных изменений в процессе работы.
Поэтому нам нужно воспользоваться таким набором инструментов, который не
будет усложнять работу при изменении взглядов на создаваемый продукт. Если мы
решили, что к таблице базы данных нужно добавить еще один столбец или изме­
нить систему переходов между страницами, нам нужно иметь возможность войти
в приложение и сделать все необходимое без объемных работ по программирова­
нию или долгой возни с настройками конфигурации. Вы сможете убедиться в том,
что, когда дело касается внесения изменений, Ruby on Rails всегда на высоте —это
идеальная среда для гибкого программирования.
Попутно мы будем создавать и поддерживать в рабочем состоянии набор те­
стов. Они помогут нам убедиться в том, что приложение всегда делает только то,
для чего оно предназначено. Rails не только допускает создание подобных тестов,
но и предоставляет вам исходный набор тестов при каждом определении нового
контроллера.
Но вернемся к приложению.

5.2. Для чего предназначен Depot


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

Примеры использования
Пример использования — это обыкновенное утверждение о том, как кто-нибудь
использует систему. Консультанты изобретают подобные фразы, чтобы навесить
ярлыки на вполне очевидные понятия, и то, что необычные слова всегда ценят­
ся выше, чем обычные, которые на самом деле куда значимее, — это издержки
бизнеса.
Примеры использования приложения Depot довольно просты (что для не­
которых прозвучит трагически). Начнем с определения двух разных ролей или
действующих субъектов: покупателя и продавца.
76 Часть II • Создание приложения

Покупатель использует Depot для просмотра продаваемого товара, выбора по­


купок и предоставления информации, необходимой для оформления заказа.
Продавец использует Depot для учета продаваемого товара, для определения
заказов, ожидающих доставки, и для пометки доставленных заказов. (Продавцы
также используют Depot, чтобы заработать кучу денег, выйти на пенсию и жить на
тропических островах, но это уже тема какой-нибудь другой книги.)
И пока это все, что нам может понадобиться. Можно, конечно, мучительно
уточнять, что такое «учет товара» и что подразумевается под «заказом, готовым
к доставке», но зачем? Если есть какие-то неясные моменты, то вскоре, по мере
демонстрации заказчику последовательных этапов нашей работы, все само собой
прояснится.
Что касается отзывов, не забудем получить их прямо сейчас, чтобы убедиться
в том, что наши начальные (весьма схематичные) примеры использования вполне
отвечают представлениям нашего пользователя. Предположив, что наши примеры
использования были приняты: давайте определим, как наше приложение будет
работать с точки зрения различных пользователей.

Последовательность страниц
В любом случае нам нужно выработать замысел устройства основных страниц на­
шего приложения и составить примерное представление о том, как пользователи
будут переходить со страницы на страницу. На ранней стадии разработки эта по­
следовательность страниц, скорее всего, будет неполной, но она все же поможет
нам сфокусироваться на том, что должно быть сделано, и определить последова­
тельность действий.
Возможно, кому-то нравится создавать макеты страниц веб-приложения
в Photoshop, Word или (как ни странно) с помощью HTML. А мне нравится ра­
ботать с карандашом и бумагой. Так быстрее, и клиент также может включиться
в работу, взяв карандаш и набросав варианты на бумаге.
Первый набросок последовательности страниц для покупателя показан на
рис. 5.1. В нем нет ничего особенного. Покупатель видит страницу каталога, на ко­
торой он поштучно выбирает товар. Каждый выбранный товар попадает в корзину,
которая демонстрируется после каждого выбора. Покупатель может продолжить
покупки, используя страницы каталога, или подтвердить содержимое корзины
и купить находящийся в ней товар. В процессе подтверждения мы получаем кон­
тактные и платежные сведения, а затем выставляем счет. Мы еще не знаем, как
будем оформлять платеж, поэтому его детали в этой последовательности не имеют
ясных очертаний.
Последовательность страниц продавца показана на рис. 5.2. Она также не от­
личается особой сложностью. После входа в программу продавец видит меню, по­
зволяющее ему создать запись о товаре или просмотреть существующую запись,
а также отправить существующие заказы. При просмотре товара продавец может
выборочно отредактировать информацию о товаре или вовсе удалить касающуюся
его запись.
Глава 5 • Интернет-магазин 77

С трани ца каталога С трани ца корзины

Рис. 5.1. Последовательность страниц, используемых покупателями

Р еги стр ац и я Меню


------- ^
Имя
* -------- -
Пароль ------------

Р ассм атриваем ы е заказы

Агеев В и к то р Иванович
г. С а н к т-П е те р б у р г

1 хкни га руб. —
2 х карандаша руб.

Способ оплаты:
Д оставить
| О тлож и ть /
( пока )
к о то ра я используется П оказать
для ввода то в а р а следующий

Рис. 5.2. Последовательность страниц, используемых продавцом


78 Часть II • Создание приложения

Вариант отправки сильно упрощен. Он предусматривает постраничный вывод


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

Данные
И, наконец, нужно подумать о данных, с которыми мы собираемся работать.
Заметьте, что здесь мы не употребляем такие слова, как «схемы» или «классы».
Мы также не говорим о базах данных, таблицах, ключах и тому подобных вещах.
Речь идет только о данных. На этой стадии разработки мы еще не знаем, будут ли
использоваться базы данных.
Похоже, что на основании примеров использования и последовательности
страниц мы будем иметь дело с теми данными, которые изображены на рис. 5.3.
На мой взгляд, и здесь намного проще будет использовать карандаш и бумагу, а не
какой-нибудь мудреный инструмент, но вы можете воспользоваться тем, что вам
больше по душе.

Товар Корзина Заказ

Подробности заказа

Д е т а л и платеж а

С о сто я н и е доставки

Рис. 5.3. Исходные предположения о составе данных, используемых в приложении

Работа с диаграммой, отображающей состав данных, наводит на некоторые


размышления. Поскольку пользователь что-то покупает, нам нужно куда-то
складывать покупки, поэтому я добавил корзину. Но что такое корзина, кроме
как место для временного храпения списка товаров, представляется весьма
смутно — я не в состоянии найти что-либо существенное, что можно было бы
в ней хранить. Для обозначения этой неопределенности я поместил внутри
прямоугольника, изображающего на диаграмме корзину, вопросительный знак.
Глава 5 • Интернет-магазин 79

Смею предположить, что эту неопределенность мы развеем в процессе создания


приложения Depot.
В результате размышлений о данных общего характера возникает вопрос, какие
сведения должны попадать в заказ. И тут я снова оставляю вопрос открытым — мы
проясним его чуть позже, как только начнем демонстрировать пользователю наши
первые шаги.
В конечном итоге вы могли заметить, что я продублировал цену товара в строке
выбора. Здесь я несколько отступаю от правила «в начальной стадии все должно
выглядеть как можно проще», но это нарушение продиктовано моим личным опы­
том. Если цена товара изменится, то это изменение не должно отражаться на том
товаре, который попал в строку выбора текущих открытых заказов, поэтому в каж­
дой строке выбора должна быть указана такая цена товара, которая существовала
на момент оформления заказа.
Здесь я снова вместе с заказчиком проверил свои рассуждения и понял, что на­
хожусь на верном пути. (Вероятнее всего, мой заказчик сидел рядом со мной, пока
я рисовал все эти три диаграммы.)

5.3. А теперь приступим


к программированию
Итак, посидев рядом с заказчиком и проведя некоторый предварительный анализ,
мы готовы сесть за компьютер и приступить к разработке с опорой на замысел,
отображенный в трех исходных диаграммах. Но вскоре мы, наверное, их просто
выбросим —они устареют, как только мы начнем получать отзывы о создаваемом
приложении. Кстати, именно поэтому мы и не стали тратить на них слишком много
времени —так будет проще с ними расстаться.
В следующих главах мы начнем создавать приложение, основываясь на нашем
текущем представлении. Но пока мы не перевернули эту страницу, нужно ответить
еще на один вопрос. С чего начать?
Я предпочитаю работать с заказчиком, поэтому мы можем выработать приори­
теты совместными усилиями. В данном случае я обратил его внимание на то, что
трудно разрабатывать что-нибудь другое, пока у нас не будет некоторого задела
основных товаров, присутствующих в системе, поэтому я предложил потратить
пару часов на то, чтобы получить начальную версию работоспособного средства
по учету товаров. И он, конечно же, согласился.

ОБЩ ИЙ СОВЕТ ПО ВОЗВРАЩ ЕНИЮ НА ПРАВИЛЬНЫ Й К У Р С -------------------------


Весь материал данной книги был протестирован. При точном соблю дении описанного в ней
сценария с использованием рекомендованных версий Rails и SQLite3 на Linux, Mac OS X или
W indow s все должно работать в соответствии с описаниями. Но могут случаться и откло­
нения от заданного маршрута. Никто из нас не застрахован от опечаток, и сопутствую щ ие
исследования не только возможны, но и категорически приветствуются. При этом следует
знать, что они могут сбить вас с нужного курса. Но не стоит этого бояться: в конкретных
разделах, где часто возникаю т подобные проблемы, будут появляться описания конкрет-
80 Часть II • Создание приложения

ных действий по возвращ ению на правильный курс. А общ ие советы на этот счет даются
прямо сейчас.
Перезапускать сервер нужно будет только в нескольких случаях, когда это действие кон­
кретно оговорено в данной книге. Но, когда вы попадете в полный тупик, можно будет
попробовать перезапустить сервер.
«Волшебная» команда rake d b :m igrate:redo, о сущ ествовании которой вам нужно знать,
подробно рассмотрена в части III. Она выполняет отмену и повторное использование по­
следней миграции.
Если ваш сервер не примет какие-нибудь введенные в ф орму данные, обновите форму на
своем браузере и повторите отправку данных.
Задача А:
создание приложения

Основные темы:
> создани е нового прилож ения;
> конф игурирование базы данны х;
> создани е м оделей и контроллеров;
> добавлен ие табли ц стилей;
> обновление разметки и представления.

Нашей первой задачей в разработке приложения станет создание веб-интерфейса,


позволяющего вести учет товаров — вводить информацию о новых товарах, ре­
дактировать сведения об уже имеющихся, удалять ненужные товары и т. д. Мы
разобьем разработку приложения на шаги, каждый из которых будет по времени
выполнения занимать всего лишь несколько минут. Итак, приступим.
Обычно наши шаги будут состоять из нескольких составляющих этапов, на­
пример шаг В будет состоять из этапов В 1, В2, ВЗ и т. д. В данном случае у шага
будет два этапа.

6.1. Шаг А1: создание приложения


по учету товаров
Основой приложения Depot является база данных. Если эта база уже установлена,
сконфигурирована и протестирована, это может избавить вас от массы проблем.
Если вы не уверены в том, что именно вам нужно, проще всего положиться на
настройки по умолчанию. Если вы уже знаете, что вам нужно, Rails упростит вам
описание выбранной конфигурации.
82 Часть II • Создание приложения

Создание Rails-приложения
В главе 2 мы уже видели, как создается новое Rails-приложение. Здесь мы сделаем
то же самое. Перейдите в окно командной строки и наберите команду r a i l s new,
указав после нее имя нашего проекта. В данном случае наш проект называется
d e p o t, поэтому убедитесь в том, что вы не находитесь в каталоге уже существую­
щего приложения, и наберите следующую команду:
work> r a i l s new depot

Мы увидим, как по экрану побегут строки вывода. Когда этот процесс завер­
шится, мы обнаружим новый каталог по имени depot. Именно с ним мы и будем
работать.
work> cd depot
depot> d ir /w
[■] [•■] .g itig n o r e [app] [config]
config.ru [db] [doc] Gemfile Gemfile. lo ck
[lib ] [lo g ] [ p u b lic ] Rakefile README
[s c r ip t ] [te s t] [tmp] [ven d o r]

Создание базы данных


Для данного приложения будет использоваться база данных с открытым исходным
кодом SQLite (которая вам понадобится, если вы намерены придерживаться пред­
ставленного программного кода). В данной книге описывается использование
SQLite версии 3.
SQLite 3 является базой данной, используемой при разработке Rails-приложений
по умолчанию, и она была установлена вместе с Rails в главе 1 «Установка Rails».
При работе с SQLite 3 не нужны никакие шаги по созданию базы данных, и поэто­
му нет никаких специальных учетных записей пользователей или паролей. Итак,
сейчас вы начинаете убеждаться в преимуществах следования общему течению
(или, в соответствии с избитой фразой пользователей Rails, следования соглаше­
нию о конфигурации).
Если вам важно использовать другой сервер базы данных, не SQLite 3, то коман­
ды, необходимые для создания базы данных, и порядок наделения полномочиями
будут другими. Ряд полезных советов на этот счет можно найти на информацион­
ном ресурсе «Getting Started Rails Guide»1.

Генерирование временной платформы


Ранее, на рис. 5.3, «Исходные предположения о составе данных, используемых
в приложении», мы дали кратное описание основного содержимого таблицы това­
ров products. Давайте воплотим все это в реальность. Нам нужно создать таблицу
базы данных и модель Rails, которая позволит нашему приложению использовать

1 http://guides.rubyonrails.Org/getting_started.htm l#configuring-a-database
Глава б • Задача А: создание приложения 83

эту таблицу, а также создать ряд представлений для формирования пользователь­


ского интерфейса и контроллер, управляющий приложением.
Итак, давайте создадим для нашей таблицы p r o d u c t s модель, представления,
контроллер и миграцию. Работая с Rails, все это можно сделать с помощью одной
команды, попросив Rails сгенерировать то, что называется временной платформой
(scaffold) для заданной модели. Заметьте, что слово в командной строке1, которая
вскоре последует, используется в форме единственного числа — P ro d u c t. В Rails
модель автоматически отображается на таблицу базы данных, чье имя является
формой множественного числа класса модели. В нашем случае мы запросили
модель под названием P ro d u c t, поэтому Rails связывает ее с таблицей по имени
p ro d u c ts . (А как же она найдет эту таблицу? Где ее искать, Rails подскажет запись
d e v e lo p m e n t в файле config/database.yml. Для пользователей SQLite 3, это будет
файл в каталоге db.)
depot> r a i ls generate sca ffo ld Product \
t i t l e : s t r i n g d e s c rip tio n :te x t im age_url: s trin g price:decim al
invoke a c tiv e _ r e c o r d
c re a te db/m igrate/20110711000001_create_products. rb
c re a te app/m odels/product. rb
invoke t e s t _ u n it
c re a te t e s t / u n it / p r o d u c t _ t e s t . rb c re a te
c re a te te s t/ fix tu re s / p ro d u c ts .y m l
ro u te resou rces :products
invoke s c a f f o ld _ c o n t r o lle r
c re a te a p p / c o n tr o lle r s / p r o d u c t s _ c o n tr o lle r . rb
invoke erb
c re a te app/view s/products
c re a te ap p /view s/p ro d u cts/in d ex . h tm l. erb
c re a te a p p / v ie w s/ p ro d u c ts/ e d it. h tm l. erb
c re a te app/view s/products/show . h tm l. erb
c re a te ap p /view s/products/new .htm l.erb
c re a te ap p /vie w s/p ro d u cts/_fo rm .htm l.e rb
invoke t e s t _ u n it
c re a te t e s t / f u n c t io n a l/ p r o d u c t s _ c o n t r o lle r _ t e s t . rb
invoke h e lp e r
c re a te a p p / h e lp e rs/p ro d u c ts_h e lp e r.rb
invoke t e s t _ u n it
c re a te te s t/ u n it/ h e lp e r s / p r o d u c t s _ h e lp e r _ te s t. rb
c re a te a p p / a s s e ts / s ty le s h e e ts / s c a ffo ld s . c s s . scss
invoke a s se ts
invoke c o ffe e
c re a te a p p / a s s e ts / ja v a s c rip ts / p ro d u c ts . j s . c o ffe e
invoke scss
c re a te a p p / a s s e ts / s ty le s h e e ts / p ro d u c ts . c s s . scss
invoke scss
c re a te a p p / a s s e ts / s ty le s h e e ts / s c a f fo ld s . c s s . scss

1 Команда из-за своей длины не может целиком поместиться но ширине страницы.


Чтобы ввести команду, располагающуюся на нескольких строках, нужно просто ста­
вить обратный слэш в качестве последнего символа на всех строках, кроме последней,
и тогда будет выдаваться приглашение на дополнительный ввод. Пользователям
Windows вместо обратного слэша нужно будет поставить символ вставки (А).
84 Часть II • Создание приложения

Генератор создает целый пакет файлов. Нас в первую очередь будет интересо­
вать файл миграции, а именно 20110711000001_create_products.rb.
Миграция представляет изменение, которое нужно внести в данные, выражен­
ное в исходном файле в терминах, независимых от применяемой базы данных. Та­
кие изменения могут обновить как схему базы данных, так и данные в ее таблицах.
Эти миграции применяются для обновления нашей базы данных, и их применение
можно отменить, чтобы база данных вернулась к прежнему состоянию. Миграциям
будет посвящена целая глава 23, а сейчас мы будем просто пользоваться ими без
излишних комментариев.
У миграций имеется префикс с отметкой времени в формате UTC (20110711000001),
имя (create_products) и расширение имени файла ( . rb, поскольку это R u b y -
программа).
Тот префикс с отметкой времени, который вы увидите, будет отличаться от это­
го. В действительности отметки времени, используемые в данной книге, не имеют
ничего общего с настоящими. Обычно ваши отметки времени не будут выстроены
в четкой последовательности, и они будут отражать время создания миграции.

Применение миграций
Хотя мы уже сообщили Rails об основных типах данных каждого свойства, давайте
пойдем дальше и уточним определение цены ( p r i c e ) , чтобы у нее было восемь
цифр в значимой части и две цифры после десятичного знака.
rails31/depot_a/db/migrate/201107110e0001_create_products. rb
c la s s C re ate Pro d ucts < A c tiv e R e c o rd ::M ig ra tio n
def change
c r e a t e _ ta b le :p rod u cts do |t|
t.s t r in g : t i t l e
t . t e x t d e s c rip tio n
t . s t r i n g :im a g e_u rl
► t.d e c im a l : p r ic e , p r e c is io n : 8, s c a le : 2

t.tim estam p s
end
end
end

После внесения изменений нужно заставить Rails применить эту миграцию


к нашей разработочной базе данных. Мы это сделаем с помощью команды ra k e .
Эта команда — надежный помощник, который всегда под рукой: вы приказываете
ей выполнить определенную задачу, и она ее исправно выполняет. В данном случае
мы приказали rak e реализовать любую миграцию, которая еще не применялась
к нашей базе данных:
depot> rake db:migrate
( i n /Users/rubys/w ork/depot)
== C re a te P ro d u c ts: m ig ra tin g =================================================
-- c r e a t e _ t a b l e ( : p ro d ucts)
-> 0.0027s
== C re a te P ro d u c ts: m igrated (0.0 02 3s) ========================================
Глава 6 • Задача А: создание приложения 85

Вот и все. R ake находит миграции, которые еще не применялись к базе дан­
ных, и применяет их. В нашем случае к базе данных, определенной в разделе
d e v e lo p m e n t файла database.yml, добавляется таблица p r o d u c t s .
Основа заложена. Мы установили наше приложение Depot в виде проекта Rails.
Мы создали разработочную базу данных и сконфигурировали наше приложение
на подключение к этой базе данных. Мы создали контроллер p r o d u c t s и модель
P ro d u c t и воспользовались миграцией для создания соответствующей таблицы
p ro d u c ts . И для нас были созданы несколько представлений. Настала пора по­
смотреть, как все это работает.

Просмотр перечня товаров


Тремя командами мы создали приложение и базу данных (или таблицу внутри
существующей базы данных, если вы выбрали не SQLite 3, а какую-нибудь другую
программу управления базами данных). Прежде чем разбираться, что произошло
за кулисами, давайте испытаем наше только что созданное приложение в работе.
Сначала нужно запустить локальный сервер, предоставляемый Rails:
depot> r a i l s server
=> B o o tin g W EBrick
=> R a ils 3 .1 .0 a p p lic a tio n s t a r t in g in development on h t t p : / / 0 .0 .0.0:3000
=> C a l l w ith -d to detach
=> C trl- C to shutdown s e rv e r
[2011-12-12 11:20:45] INFO W EBrick 1 .3.1
[2011-12-12 11:20:45] INFO ruby 1 .9 .2 (2011-07-09)[i386-mingw32]
[2011-12-12 11:20:46] INFO W E B ric k ::H T T P S e rv e r# s ta rt: pid=4908 port=3000

Эта команда запускает веб-сервер на нашем локальном хосте с использованием


порта 3000, то есть происходит то же самое, что было с нашим приложением demo,
рассмотренным в главе 2. Если при попытке запуска сервера будет получено со­
общение об ошибке «Address already in use» (адрес уже используется), это будет
означать, что на данной машине уже есть запущенный сервер Rails. Если вы вы­
полняли все упражнения, предлагаемые в данной книге, сервер мог быть запущен
для приложения «Hello, World!» из главы 2. Нужно найти консоль этого сервера
и прекратить его работу с помощью комбинации клавиш Ctrl+C. Если вы работаете
в Windows, при этом можно увидеть приглашение на подтверждение: «Завершить
выполнение пакетного файла [У(да)/Ы(нет)]?» (Terminate batch job (Y/N)?). Если
оно появится, ответьте у.
Давайте подключимся к серверу. Вспомним, что URL-адрес, вводимый в наш
браузер, содержит номер порта (3000) и имя контроллера в нижнем регистре
( p ro d u c ts ) (рис. 6.1).
Поскольку перечень товаров пуст, ничего интересного мы там не увидим. Да­
вайте этот перечень чем-нибудь наполним. Щелчок на ссылке New product (Новый
товар) приведет к появлению формы, которую нужно будет заполнить (рис. 6.2).
Эти формы являются простыми H TM L-шаблонами, похожими на те, которые
создавались в разделе 2.2 «Привет, Rails!». Мы можем их изменить. Давайте из­
меним количество строк в поле d e s c r i p t i o n (описание):
86 Часть II • Создание приложения

, (t) Depot
4- С й lo ca l host A" ☆ A

Listing products
T itle D e s c r ip t io n I m a g e uri P ric e

Рис. 6.1. Подключение к серверу

(3

f С A lo c a lh o s t - :Я * Л,
>
(-;.i
New product
T itle

j D e s c r ip tio n

Рис. 6.2. Форма для заполнения

rails31/depot_a/app/view s/products/_form .htm l.erb


<%= form_for((a>product) do |f | %>
<% i f (S p ro d u c t.e rro rs .a n y ? %>
<div id = "e rro r_ e x p la n a tio n ">
<h2><%= p luralize((S )p ro d u ct . e r r o r s . count, " e r r o r " ) %>
p ro h ib ite d t h i s product from being saved:</h2>

<ul>
Глава б • Задача А: создание приложения 87

<% (fflp ro d u c t.e rro rs,fu ll_m e s sa g e s. each do |msg| %>


<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>

<div class= "field ">


<%= f . l a b e l : t i t l e %><br />
<%= f. t e x t _ f ie ld : t i t l e %>
</div>
<div class= "field ">
<%= f . l a b e l :d e s c r ip t io n %><br />
► <%= f . t e x t _ a r e a : d e s c r ip tio n , rows: 6 %>
</div>
<div class= "field ">
<%= f . l a b e l :im a g e_u rl %><br />
<%= f. t e x t _ f ie ld :im a g e _u rl %>
</div>
<div class= "field ">
<%= f . l a b e l :p r ic e %><br />
<%= f. t e x t _ f ie ld -.price %>
</div>
<div c la s s = "a c tio n s ">
<%= f.s u b m it %>
</div>
<% end %>

Более подробно все это будет рассмотрено в главе 8 «Задача В: Отображение


каталога товаров». А сейчас, чтобы понять, что это такое, мы внесли изменение
в одно из полей. Теперь продолжим работу и заполним форму (рис. 6.3).
Щелкните на кнопке Create (Создать), и вы увидите, что запись о новом товаре
будет успешно создана. Если теперь щелкнуть на кнопке Back (Назад), вы увидите
новый товар в перечне (рис. 6.4).
Может быть, это и не самый красивый интерфейс, по он работает, и мы можем
предъявить его на утверждение нашему заказчику. Он может оценить работу других
ссылок (показывающих подробности, позволяющих редактировать существующие
сведения о товарах и т. д.). Нужно объяснить, что это всего лишь первый шаг и мы
знаем, что он далек от совершенства, но нам хотелось получить отзыв заказчика
как можно раньше. (И в какой-нибудь другой книге четырех команд для этого было
бы, наверное, недостаточно.)
Всего лишь четыре команды позволили нам выполнить довольно большой
объем работы. Прежде чем продолжить, попробуем воспользоваться еще одной
командой:
rake t e s t

В выводе этой команды должны присутствовать две строки, в каждой из кото­


рых сообщается: 0 f a i l u r e s , 0 e r r o r s (0 сбоев, 0 ошибок). Это относится к блоч­
ному (unit) и функциональному (functional) тестам, которые Rails генерирует
вместе с созданием временной платформы. Пока эти тесты имеют минимальный
88 Часть II • Создание приложения

4- С Я O tocalhw t p ro d u c ts /n e w a, w* A.

New product
I T itle
CoffeeScript

1 D e s c r ip tio n
< p > C o f f e e S c r i p t i s J a v a S c r i p t done *
r i g h t . I t p rovides a l l of J a v a S c r i p t 's
f u n c t i o n a l i t y w ra ppe d i n a c l e a n e r , more “ i,
s u c c i n c t s y n t a x . I n t h e f i r s t bo ok on
t h i s e x c i t i n g new l a n g u a g e , C o f f e e S c r i p t »
g u r u T r e v o r Burnham shows you how t o
:
j Im a g e url
1 cs.jpg

I P ric e
36.00

Create Product

w......................... :................ ......... ................... -----

Рис. б.З. Форма заполнена

'Q D epot ^ ' __________■............. __________________________

* • -> С Л lo c o lh o s f у p io d u iB * is \ j

Listing products
Im age „ .
T itle D e s c r ip tio n , P ric e
url
< p > C o ffe e S c rip t is J a v a S c r ip t d o n e rig h t. It
p ro v id e s all o f J a v a S c r ip t 's f u n c t io n a lit y w ra p p e d
in a c le a n e r, m o re s u c c in c t s y n t a x . In th e firs t
C o f f e e S c r ip t b o o k o n t h is e x c itin g n e w la n g u a g e , C o f f e e S c r ip t c s .jp g 3 6 ,0 S h o w E d it С
g u ru T r e v o r B u rn h a m s h o w s y o u h o w to h o ld o n t о
ail t h e p o w e r a n d fle x ib ility o f J a v a S c r ip t w h ile
w ritin g c le a re r , c le a n e r, a n d s a f e r c o d e ,< /p >

New P ro d u c t

Рис. 6.4. Появился новый товар

объем, но сам факт их присутствия и успешного прохождения должен вселить в вас


уверенность. По мере чтения глав части II запуск этой команды будет предлагаться
довольно часто, поскольку это поможет вам выявить и отследить ошибки. Более
Глава 6 • Задача А: создание приложения 89

подробно этот вопрос будет рассмотрен в разделе 7.2 «Шаг Б2: Блочное тестиро­
вание моделей».
Следует учесть, что при использовании базы данных, отличной от SQLite3,
тестирование может не пройти. Проверьте содержимое своего файла database.yml
и изучите материал главы 24.

6.2. Шаг А2: улучшение внешнего вида


перечня товаров
У заказчика возникло еще одно требование (заказчикам всегда что-нибудь не
нравится). По его мнению, перечень товаров имеет слишком неприглядный вид.
Не можем ли мы его несколько «приукрасить»? И, если уж мы будем этим зани­
маться, нельзя ли наряду с URL-адресом изображения вывести само изображение
торлра?
Здесь у нас возникает дилемма. Как разработчики, мы учимся воспринимать
подобные запросы, сделав резкий вдох, понимающе кивнув головой и вкрадчиво
спросив: «А что бы вы хотели увидеть?» В то же время нам хочется продемон­
стрировать все, на что мы способны. В конце концов на первый плаи выходит та
легкость, с которой подобные изменения делаются в Rails, и мы запускаем свой
испытанный текстовый редактор.
Но, перед тем как углубиться в работу, было бы неплохо обзавестись после­
довательным набором тестовых данных, с которыми можно работать. Мы можем
воспользоваться нашим сгенерированным при создании временной платформы
интерфейсом и ввести данные прямо в браузере. Но, если мы это сделаем, будущие
разработчики, работающие с основой нашего кода, вынуждены будут делать то же
самое. И если мы работаем над этим проектом, представляя при этом только часть
команды, каждому ее участнику придется вводить свои собственные данные. Было
бы неплохо, если бы данные в таблицу можно было загрузить более управляемым
способом. Оказывается, это в наших силах. Rails предоставляет нам возможность
импортировать исходные данные.
Сначала мы просто внесем изменения в файл seeds.rb, который находится в ка­
талоге db.
Затем мы добавим код для заполнения таблицы p roducts. Для этого воспользу­
емся методом c r e a t e ( ) модели Product. Следующий код является извлечением из
вышеупомянутого файла. Вместо того, чтобы набирать содержимое файла вручную,
можно загрузить файл из образца кода, имеющегося в Интернете1.
А заодно можно загрузить изображения2 и поместить их в каталог app/assets/
images вашего приложения. Но вы должны знать, что сценарий seeds.rb перед за­
грузкой новых данных удаляет из таблицы p ro d u c ts все ранее находившиеся в ней
данные. Если вы уже потратили несколько часов на ручной ввод своих данных в это
приложение, возможно, вам не захочется запускать этот сценарий!

1 http://media.pragprog.com/titles/rails4/code/depot_b/db/seeds.rb
2 http://media.pragprog.com/titles/rails4/code/rails31/depot_b/app/assets/images/
90 Часть II • Создание приложения

rails31/depot_b/db/seeds. rb
P ro d u c t. d e l e t e _ a l l
# . . .
P ro d u c t. c r e a t e ( t i t l e : 'Programming Ruby 1 . 9 ' ,
d e s c r ip t io n :
%{<p>
Ruby is th e f a s t e s t growing and most e x c it in g dynamic language
out th e re . I f you need to get working programs d e liv e re d f a s t ,
you should add Ruby to your to o lb o x .
</p>b
im ag e _u rl: 'r u b y . jp g ',
p r ic e : 49.95)
# . . .

Обратите внимание на то, что в коде используется элемент синтаксиса %{...},


являющийся альтернативой строковых литералов, взятых в двойные кавычки. Эта
альтернатива удобна для использования с длинными строками. Также обратите
внимание на то, что при использовании принадлежащего Rails метода c r e a t e ( )
произойдет молчаливый отказ в случае невозможности вставки записей в базу
данных из-за ошибок проверки данных.
Для заполнения таблицы p r o d u c t s тестовыми данными нужно просто запу­
стить следующую команду:
depot> rake db:seed
Теперь давайте приведем в порядок перечень товаров. Работа будет состоять
из двух частей: определения набора стилевых правил и подключения этих правил
к странице путем определения на ней HTML-атрибута c la s s .
Нам нужно место, куда будут помещены наши определения стиля. Поскольку
мы продолжаем работать с Rails, для нас в этой среде на данный счет есть соглаше­
ние, и ранее выданная команда g e n e r a te s c a f f o l d i n g уже заложила всю нужную
основу. Раз так, мы можем продолжить работу, заполняя пока еще пустую таблицу
стилей products.css.scss, которая находится в каталоге app/assets/stylesheets.

rails31/depot_b/app/assets/stylesheets/products. c s s . scss
// Сюда помещаются все определения стилей для контроллера P ro d u cts.
// Они будут автоматически включены в файл a p p lic a t io n . c s s .
// Что такое Sass (S C S S ), можно узн ать здесь: h ttp :// sa ss- lan g .co m /

.p ro d u cts {
t a b le {
b o rd e r- c o lla p s e : c o lla p s e ;
}
ta b le t r td {
padding: 5px;
v e r t i c a l - a l i g n : to p ;
}
.lis t_ im a g e {
w id th : 60px;
h e ig h t: 70px;
}
Глава 6 • Задача А: создание приложения 91

. li s t _ d e s c r i p t i o n {
w id th : 60%;

dl {
m argin: 0;
}
dt {
c o lo r : #244;
fo n t- w e ig h t: bold;
fo n t - s iz e : la r g e r ;
}
dd {
m argin: 0;
}
}
. lis t _ a c t io n s {
fo n t - s iz e : x -sm all;
t e x t - a lig n : r ig h t ;
p a d d in g - le ft: lem;
}
. lis t _ li n e _ e v e n {
background: # e0f8f8;
}
. lis t _ lin e _ o d d {
background: #f8b0f8;
}

Если внимательно изучить эту таблицу стилей, можно заметить, что CSS-
правила вложены друг в друга и правило для d l определено внутри правила для
. l i s t _ d e s c n i p t i o n , которое, в свою очередь, определено внутри правила для
products. Тем самым правила избавляются от лишних повторений, их становится
легче читать, понимать и обслуживать.
Пока вы были знакомы только с тем фактом, что в файлах, заканчивающихся на
erb. происходит предварительная обработка встроенных выражений и инструкций
Ruby. А эти файлы, если вы заметили, заканчиваются на scss, и вы, наверное, уже
догадались, что данные файлы, перед тем как обслуживаться в качестве файлов
css, проходят предварительную обработку в качестве продукта Sassy CSS1. И вы
абсолютно правы!
Как и в случае с ERb, SCSS не конфликтует с написанным по всем правилам
кодом CSS. Роль SCSS заключается в предоставлении дополнительного синтаксиса,
позволяющего упростить разработку и обслуживание таблиц стилей. В ваших же
интересах SCSS конвертирует все это в стандартный CSS, который понимает ваш
браузер.

http://sass-lang.com /
92 Часть II • Создание приложения

И наконец, нам нужно определить класс p ro d u c ts, используемый этой таблицей


стилей. Если посмотреть на уже созданные файлы .html.erb, каких-либо ссылок на
таблицы стилей вы в них не найдете. Вы даже не найдете HTML-раздел <head>,
в котором обычно находятся такие ссылки. Вместо этого в Rails имеется отдель­
ный файл, используемый для создания стандартной среды окружения страниц для
всего приложения. Этот файл по имени application.html.erb, является макетом Rails
и находится в каталоге layouts:

rails31/depot_b/app/view s/layouts/application. htm l. erb


Строка
1 <! DOCTYPE html>
- <html>
- <head>
< title > D e p o t< /title >
5 <%= s t y le s h e e t _ lin k _ t a g " a p p lic a t io n " %>
<%= ja v a s c r ip t _ in c lu d e _ t a g " a p p lic a t io n " %>
<%= c srf_m e ta _ta g s %>
- </head>
- cbody class='<%= c o n t r o lle r .c o n t r o lle r _ n a m e %>'>
10
- <%= y i e ld %>

- </body>
- </html>

В строке 5 используется вспомогательный метод stylesheet_link_tag(), предназна­


ченный для создания HTML-тега < lin k> . Этот тег заставляет браузер запрашивать
таблицу стилей приложения из каталога app/assets/stylesheets:

ra ils3 1 /d ep ot_ b /a p p / asse ts/style sh e e ts/a p p licatio n . css


/*
* Это файл-декларация, автоматически включающий все таблицы стилей,
* доступные в данном каталоге и во всех его подкаталогах.
* Вы можете добавлять к этому файлу стили, распространяемые на все приложение,
* и они будут п оявляться в верхней части скомпилированного файла, но лучше
* все же для каждой области видимости стиля создать новый файл.
*= r e q u ir e _ s e lf
*= re q u ir e _ tr e e .
*/
Как объясняется в комментариях, этот файл-декларация автоматически вклю­
чит все таблицы стилей, доступные в этом каталоге и всех его подкаталогах. Это
действие выполняется благодаря директиве r e q u ir e _ tr e e .
Вместо этого можно перечислить имена отдельных таблиц стилей, ссылки
на которые должны быть указаны методом stylesheet_link_tag(), но, поскольку
мы находимся в макете всего приложения и поскольку этот макет уже настроен
на загрузку всех таблиц стилей, пока мы все оставим как есть. Более подробно
остальная часть файла будет рассмотрена в разделе 8.2 «Шаг В2: Добавление
макета страницы».
Глава 6 • Задача А: создание приложения 93

Поскольку мы собрались загрузить все таблицы стилей сразу, нам нужно со­
глашение для ограничения распространения правил, указанных для контроллера,
только на те страницы, которые связаны с этим контроллером. Выполнить эту зада­
чу можно простым указанием в качестве имени класса значения controller_nam e,
что мы здесь и сделали.
Теперь, когда все таблицы стилей находятся на своем месте, мы воспользуемся
простым табличным шаблоном, отредактировав файл index.htmi.erb в каталоге арр/
views/products и заменив тем самым представление, сгенерированное при создании
временной платформы:

rails31/depot_b/app/views/products/index. htm l. erb


< h l> L istin g products</hl>

<table>
<% @ products.each do |product| %>
<tr class="<%= c y c l e ( ' li s t _ l i n e _ o d d ' , ' l i s t _ l i n e _ e v e n ' ) %>">

<td>
<%= im a g e _ta g (p ro d u ct. im ag e_u rl, c la s s : ' lis t _ im a g e ' ) %>
</td>

<td c la s s = " lis t _ d e s c r ip t io n " >


<dl>
<dt><%= p r o d u c t . t i t le %></dt>
<dd><%= t r u n c a t e (s t r ip _ t a g s ( p r o d u c t . d e s c r ip t io n ) ,
le n g th : 80) %></dd>
</dl>
</td>

<td c la s s = " lis t _ a c t io n s " >


<%= lin k _ t o 'S h o w ', product %><br/>
<%= lin k _ t o ' E d i t ' , e d it_ p ro d u c t_ p a th (p ro d u c t) %><br/>
<%= lin k _ t o 'D e s t r o y ', product,
confirm: 'A re you s u r e ? ',
method: : d e le te %>
</td>
</tr>
<% end %>
</table>

<br />

<%= lin k _ t o 'New p ro d u c t', new_product_path %>

Даже в этом простом шаблоне используется ряд встроенных свойств Rails:


О Строки в перечне имеют чередующиеся фоновые цвета. Эго делается с по­
мощью вспомогательного метода Rails путем установки для каждой строки
либо класса l i s t _ l i n e _ e v e n , либо класса l i s t _ li n e _ o d d , что приводит
к автоматическому переключению двух имен стилей для последовательных
строк.
94 Часть II • Создание приложения

О Вспомогательный метод t r u n c a t e Q используется для отображения толь­


ко первых восьмидесяти символов описания. Но перед вызовом метода
t r u n c a t e ( ) мы вызываем метод s t r i p _ t a g s ( ), чтобы убрать из описания
HTM L-теги.
О Обратите внимание на то, что у строки ссылки l i n k _ t o ' D e s t r o y ' есть
параметр confirm : ' Вы уверены ? '. Если щелкнуть на данной ссылке, R ails
подстроится под ваш браузер для вывода диалогового окна, запрашивающего
подтверждение на переход по ссылке и удаление записи о товаре. (Некоторые
внутренние тонкости данного действия раскрыты в ближайшей врезке.)
Итак, мы загрузили в базу данных тестовые данные, переписали файл index,
html.erb, отображающий перечень товаров, добавили таблицу стилей application,
css.scss, и эта таблица стилей была загружена в нашу страницу с помощью файла
разметки application.html.erb. Теперь вернемся в браузер и укажем в нем адрес http://
localhost:3000/products. Появившийся в нем перечень товаров может иметь следую­
щий вид:

L is tin g p ro d u c t s
C c if ie a S c iip t
CofteeScnpt is JavaScript 1ooe rgh? !t provides Ш
all of JavaScript. ШПШ

Program m in g R uby 1.9


Ruby s tht> f a s te s t growing and m o^ t exciting
i dynamic language .

Raib Test Prescriptions Ss*


как P r e s c r ip t * is a taa
gutdo to costing ... D&svoy
•ж

Итак, мы с гордостью показываем заказчику новый перечень товаров, который


его вполне удовлетворяет. Теперь пора приступить к созданию электронной ви­
трины.

6.3. Наши достижения


В данной главе мы заложили основы для интернет-магазина:
0 Создали базу данных, предназначенную для разработки.
0 Использовали миграцию для создания и модификации схемы базы данных,
предназначенной для разработки.
Глава 6 • Задача А: создание приложения 95

0 Создали таблицу товаров p r o d u c t s и воспользовались генератором времен­


ной платформы (scaffold) для создания приложения, с помощью которого
можно вести учет товаров в этой таблице.
0 Мы дополнили разметку, предназначенную для всего приложения, а также
представление для конкретного контроллера, чтобы показать перечень то­
варов.

ЧТО ТАКОЕ METHOD: :D E LE TE?------------------------------------------------------------------


Нетрудно заметить, что сгенерированная при создании временной платф ормы ссылка «Уда­
лить» вклю чает параметр method: :delete. Этот параметр определяет метод, вызываемый
в классе ProductsController, а такж е влияет на используемый HTTP-метод.
Браузеры использую т протокол HTTP для обмена данными с серверами. В протоколе HTTP
определяется набор глаголов, которым браузер мож ет воспользоваться, и определяется,
когда и какой глагол мож ет быть использован. Обычная гиперссылка, к примеру, исполь­
зует запрос HTTP GET. Этот запрос определен протоколом HTTP для извлечения данных
и не должен иметь никаких побочных эффектов. Использование данного параметра именно
таким образом показывает, что для данной гиперссылки будет использован метод HTTP
DELETE. Rails использует эту информацию для определения, какому действию в контролле­
ре направить этот запрос.
Следует заметить, что при использовании внутри браузера, Rails заменит методы PUT
и DELETE методом POST HTTP в процессе присоединения дополнительного параметра, что­
бы марш рутизатор мог определить суть исходных намерений. В любом случае запрос не
будет кэширован или вызван поисковым агентом Интернета.

Все сделанное нами не потребовало больших усилий и настроило нас на актив­


ное продолжение работы. Базы данных жизненно важны для данного приложения,
но они не должны вас чем-то настораживать: в большинстве случаев выбор базы
данных можно отложить на будущее и приступить к работе, используя ту базу
данных, которая предоставлена Rails по умолчанию.
На данном этапе более важно правильно понять саму модель разработки. Как
вскоре выяснится, простой выбор типов данных не всегда дает полное представ­
ление обо всех свойствах модели, даже в небольших приложениях, но с этим нам
еще предстоит разобраться.

Чем заняться на досуге


Попробуйте проделать все это без посторонней помощи:
О Если вы уверены в своих силах, можете поэкспериментировать с откатом
миграции. Просто наберите следующую команду:
depot> rake db :rollb ack

Ваша схема переместится назад во времени, и таблица p r o d u c t s исчезнет.


Повторный вызов команды r a k e db: m i g r a t e создаст эту таблицу заново.
Может также понадобиться заново заполнить таблицу тестовыми данными.
Дополнительная информация о миграциях может быть получена в главе 23
«Миграции».
96 Часть II • Создание приложения

О Вопрос управления версиями уже затрагивался в соответствующем разделе


главы 1, а теперь у нас появился серьезный повод для сохранения нашей
работы. Если вы выбрали для этой цели систему Git (которую я, кстати, на­
стоятельно рекомендую), сначала придется немного заняться настройками
конфигурации — в основном придется всего лишь предоставить свое имя
и адрес электронной почты. Обычно они попадают в ваш личный каталог
с файлом .gitconfig. Наши данные имеют следующий вид:
[u s e r]
name = Sam Ruby
em ail = ru b y s @ in te rtw in g ly .n e t

Проверить конфигурацию можно с помощью следующей команды:


depot> g it repo-config --get-regexp user.*

Rails также предоставляет файл по имени .gitignore, сообщающий системе


Git о тех файлах, которые не попадают под управление версиями:

rails31/depot_b/. g itig n o re
. bundle
d b /*. s q lit e 3
lo g / * .lo g
tmp/
. sass-cache/

Из-за того, что имя этого файла начинается с точки, операционные системы
на основе Unix не показывают его в листингах каталогов. Чтобы увидеть
этот файл, нужно воспользоваться командой I s -а. Теперь все настройки
завершены, и осталось только инициализировать репозиторий, добавив все
файлы и передав их с сообщением о передаче:
depot> g it i n i t
depot> g it add .
depot> g it commit -m "Depot S c a ffo ld "

Возможно, на данном этапе все это не кажется вам слишком увлекательным,


но значение использования данной системы заключается в том, что у вас
появляется возможность для свободного экспериментирования. Если будет
переписан или удален не предназначавшийся для этого файл, можно будет
вернуться к данной точке с помощью всего лишь одной команды:
depot> g it checkout
Задача Б: проверка
приемлемости данных
и блочное тестирование

Основные темы:
> выполнение проверки приемлемости данны х и вывод сообщ ений об ошибках;
> блочное тестирование.

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

7.1. Шаг Б1: проверка


приемлемости данных
Когда заказчик поработал с результатом, достигнутым при выполнении шага А1,
он кое-что заметил. Если он вводил неверную цену или забывал давать товару
описание, приложение без малейших сомнений принимало введенную форму и до­
бавляло строку в базу данных. Отсутствие описания —это, конечно, еще полбеды,
но вот нулевая цена —это уже вопрос денег заказчика, поэтому он попросил ввести
в приложение проверку приемлемости данных. Товар не должен попадать в базу
данных, если у него пустое поле названия или описания, или же неверный URL-
адрес изображения, или неправильная цена.
98 Часть И • Создание приложения

Итак, куда же нам вставить проверку? Привратником между миром программ­


ного кода и базой данных является уровень модели. Все, что в нашем приложении
выходит из базы данных и попадает туда на хранение, не проходит мимо модели.
Поэтому модель становится идеальным местом, куда можно вставить проверку.
И неважно, откуда поступают данные — из формы или в результате программных
манипуляций в нашем приложении. Если модель проверяет данные перед их за­
писью в базу данных, тогда эта база будет защищена от некачественных данных.
Взглянем еще раз на исходный код класса модели (который находится в файле
app/models/product.rb):
c la s s Produ ct < A c tiv e R e c o rd : : Base

В добавлении проверки данных не должно быть ничего лишнего. Начнем с про­


верки наличия какого-нибудь содержимого во всех текстовых полях перед записью
строки в базу данных. Для этого добавим в существующую модель следующую
строку кода:
v a lid a t e s :t it le , d e s c r ip tio n , :im a g e _u rl, presen ce: tr u e

Метод v a l i d a t e s () является в Rails стандартным средством проверки (вали­


датором). Он будет проверять одно или несколько полей модели на соблюдение
одного или нескольких условий.
Часть инструкции p r e s e n c e : t r u e предписывает валидатору проверять наличие
данных в каждом из указанных полей. На рис. 7.1 можно увидеть, что получится,
если мы попробуем отправить сведения о новом товаре, не заполнив ни одного поля.
Картина будет весьма впечатляющей: поля с ошибками будут выделены, а сводка об
ошибках помещена в красочном списке в верхней части формы. Неплохо для всего
одной строки кода. Также можно заметить, что после редактирования и сохранения
файла productrb перезапускать приложение для проверки изменений не пришлось —
перезагрузка страницы, заставившая ранее Rails заметить изменения в схеме данных,
всегда приводит к тому, что используется самая последняя версия кода.
Нужно также проверить, что цена имеет допустимое, положительное число­
вое значение. Для проверки воспользуемся методом с выразительным именем
n u m e r i c a l i t y . Мы также передадим несколько многословному методу g r e a t e r _
t h a n _ o r _ e q u a l _ t o (больше чем или равно) значение 0.01:
v a lid a t e s : p r ic e , n u m e r ic a lit y : {g re a te r_ th a n _ o r_ e q u a l_ to : 0 .0 1 }

Теперь, если добавить товар с недопустимой ценой, появится соответствующее


сообщение, показанное на рис. 7.2.
Почему проверка велась относительно значения 0.01, а не нуля? Потому что
можно ведь ввести в это поле и такое значение, как 0.001. Поскольку база данных
сохраняет только две цифры после десятичной точки, в ней окажется нуль, даже
если поле пройдет проверку на ненулевое значение. Проверка того, что число, по
крайней мере, равно 0.01, гарантирует, что будет сохранено только допустимое
значение.
Нам нужно проверить еще два элемента. Сначала нам нужно убедиться в том,
что у каждого товара есть свое уникальное название. Эта задача решается с по-
Глава 7 • Задача Б: проверка приемлемости данных и блочное тестирование 99

New product
3 errors prohibited this product from being saved:

■ T itle c a n 't b e b lank


■ D e sc rip tio n c a n 't b e b la n k
■ Im a g e url c a n 't b e b la n k

Рис. 7.1. Проверка наличия данных в полях

мощью еще одной строки кода в модели P r o d u c t . Проведем простую проверку,


гарантирующую, что никакая другая строка в таблице p r o d u c t s не имеет такого
же названия, которое указано в той строке, которую мы собираемся сохранить:
v a lid a t e s :t it le , uniqueness: tru e

И наконец, нам нужно проверить приемлемость введенного URL-адреса изобра­


жения. Для этого воспользуемся методом fo rm a t, который определяет соответствие
значения поля регулярному выражению. В данный момент мы просто проверим,
что URL-адрес заканчивается одним из расширений: . g i f , . j p g или . png.
v a lid a t e s :im a g e _u rl, a llo w _ b la n k : t r u e , fo rm at: {
w ith : % r { \ . ( g i f | j p g | p n g ) $ } i ,
message: 'URL должен указы вать на изображение формата G IF , 3PG или PNG.'
}
100 Часть II • Создание приложения

Depot

4" С Л О localhostSOOO/products А& Л

New product

■ P rice is n o t a n u m b e r

T itle
Program ming Ruby 1.9

D e s c r ip tio n
<p> Ruby i s t h e f a s t e s t g r o w in g and most
e x c i t i n g dyna mic la n g u a g e o u t t h e r e . I f
you n e e d t o g e t w o rk in g pro g r am s d e l i v e r e d
f a s t , you s h o u l d a dd Ruby t o y o u r t o o l b o x .
</p>

Im a g e url
ruby jpg

]
Create Product

B ack

Рис. 7.2. Цена не проходит проверку на приемлемость

Может быть, позже нам захочется изменить эту форму, дав пользователю воз­
можность выбора из перечня доступных изображений, но проверка приемлемости
все равно будет сохранена, чтобы злоумышленники не смогли отправить неверные
данные непосредственно из этого поля.
Итак, всего за пару минут мы добавили следующие виды проверок:
О поля названия, описания и URL-адреса изображения не оставлены пустыми;
О цена является допустимым числом, значение которого не меньше 0.01;
О название имеет уникальное значение среди всех товаров;
О URL-адрес изображения выглядит вполне приемлемо.
Обновленная модель P r o d u c t должна приобрести следующий вид:

rails31/depot_b/app/models/product. rb
c la s s Product < A c tiv e R e c o rd : : Base
v a lid a t e s : t i t l e , d e s c r i p t i o n , :im a g e _u rl, presen ce: tr u e
v a lid a t e s :p r ic e , n u m e r ic a lit y : {g r e a t e r _ th a n _ o r _ e q u a l_ to : 0 .0 1 }
v a lid a t e s : t i t l e , uniqueness: tr u e
Глава 7 • Задача Б: проверка приемлемости данных и блочное тестирование 101

v a lid a t e s :im a g e _u rl, a llo w _ b la n k : t r u e , fo rm at: {


w ith : % r { \ . ( g i f | j p g | p n g ) $ } i ,
message: 'must be a URL f o r G IF , DPG or PNG im a g e .'
# URL должен указы вать на изображение формата G IF , UPG или PNG
}

Ближе к завершению данного цикла мы попросили заказчика поработать с при­


ложением, и он остался доволен. Все это заняло лишь несколько минут, но простое
добавление проверки приемлемости данных придало страницам ведения перечня
товаров более внушительный вид.
Прежде чем двигаться дальше, еще раз протестируем приложение:
rake t e s t

Как ни странно, на этот раз тест не прошел. Причем в двух местах: в s h o u ld


c r e a t e p r o d u c t (нужно создать товар) и в s h o u ld u p d a te p r o d u c t (нужно обно­
вить товар). Понятно, что какие-то наши действия вынуждают что-нибудь сделать
с созданием и обновлением товаров. Здесь нет ничего неожиданного. Если поду­
мать, разве это в конечном счете является самым главным в проверке приемлемости
данных?
Проблема решается предоставлением допустимых тестовых данных в файле
test/functional/produ cts_co ntro 11е r_test.r b:

ra ils3 1 / d e p o t_ c/ te st/ fu n ctio n a l/ p ro d u cts_ co n tro lle r_ te s t.rb


re q u ire 't e s t _ h e lp e r '

c la s s P ro d u c ts C o n tro lle rT e s t < A c tio n C o n tro lle r ::T e s tC a s e


setup do
^product = p ro d u c ts (:o n e )
► (Supdate = {
► t i t l e : ' Lorem Ip su m ',
► d e s c r ip tio n : 'W ib b les are f u n ! ',
► im ag e_u rl: 'lo r e m .jp g ',
► p r ic e : 19.95
► }
end

t e s t "should get in dex " do


get : index
a s se rt_re sp o n s e : success
a s s e r t _ n o t _ n il a s s ig n s (:p ro d u c ts )
end

t e s t "should get new" do


get :new
as se rt_re sp o n s e : success
end

t e s t "should c re a te p rod uct" do


a s s e r t _ d if f e r e n c e ( ' P ro d u c t. c o u n t' ) do
► post :c r e a t e , p ro d uct: (Supdate
102 Часть II • Создание приложения

end

a s s e r t _ r e d ir e c te d _ to p ro d u c t_ p a th (a s s ig n s (: p ro d u c t))
end

# ...
t e s t "should update p rod uct" do
► put :update, id : ^ product,to _param , p ro d uct: @update
a s s e r t _ r e d ir e c te d _ to p ro d u c t_ p a th (a s s ig n s (: p ro d u c t))
end

# ...
end

После внесения этих изменений следует перезапустить тесты, и они отрапор­


туют о том, что все нормально. Но все это означает, что мы ничего не испортили.
А нам нужно нечто большее. Теперь нужно убедиться в том, что только что добав­
ленный код проверки приемлемости не только работает, но и будет работать после
внесения будущих изменений. Более подробно функциональные тесты будут рас­
смотрены в разделе 8.4 «Шаг В4: Функциональное тестирование контроллеров».
А теперь настала пора написать несколько блочных тестов.

7.2. Шаг Б2: блочное тестирование


моделей
Одно из реальных преимуществ среды Rails заключается в том, что тестирование
в ней «выпекается» с запуском каждого нового проекта. Как мы уже видели, среда
Rails приступает к созданию для вас тестовой инфраструктуры с момента создания
нового приложения с помощью команды r a i l s .
Заглянем в содержимое подкаталога unit:
depot> d i r t e s t \ u n it /w
[.] [..] .g itk e e p [h e lp e r s ]
p ro d u c t_ t e s t. rb

product_ test.rb является файлом, который Rails создает, чтобы хранить блочные
тесты для модели, ранее созданной с помощью сценария generate. Неплохо для на­
чала, но это все, чем может помочь Rails.
Посмотрим, что полезного для тестирования Rails сгенерировала внутри файла
test/unit/product_test.rb при создании данной модели:

rails3 1 /d ep ot_ b /te st/ u n it/ p ro d u ct_ te st. rb


re q u ire ' t e s t _ h e lp e r '

c la s s Pro d u ctT est < A c tiv e S u p p o rt: :T estCase


# t e s t "th e t r u t h " do
# a s s e rt tr u e
# end
end
Глава 7 • Задача Б: проверка приемлемости данных и блочное тестирование 103

Сгенерированный P r o d u c t T e s t является подклассом A c t i v e S u p p o r t :: TestCase.


Тот факт, что A c t i v e S u p p o r t : : T e s t C a s e является подклассом класса
T e s t : : U n i t : : T e s t C a s e , свидетельствует о том, что Rails генерирует тесты на
основе структуры T e s t : :U n it, которая поступает в предустановленном виде вместе
с Ruby. Для нас это хорошая новость, поскольку это означает, что если мы уже те­
стировали наши Ruby-программы с помощью тестов T e s t :: U n i t (а кто же от этого
откажется?), то эти знания можно взять за основу тестирования Rails-приложений.
Но если опыта работы с T e s t : : U n i t нет, ничего страшного. Постепенно вы все
поймете.
Внутри рассматриваемого теста Rails сгенерировала один-единственный заком­
ментированный тест под названием " t h e t r u t h " . Синтаксис t e s t . . . do поначалу
может показаться необычным, но здесь Active Support объединяет метод класса,
необязательные круглые скобки и блок, определяющий тест-метод, тем самым
всего лишь немного упрощая вашу работу. Иногда именно такие мелочи и имеют
значение.
Строка a s s e r t в данном методе, по сути, и является настоящим тестом. Толку
от него, конечно, немного, все, что он делает, — это проверяет, что истина ( t r u e )
является истиной. Понятно, что это всего лишь заполнитель, предназначенный для
замены настоящим тестом.

Настоящий блочный тест


Давайте займемся тестированием проверки приемлемости данных. В первую оче­
редь, если товар будет создан без указания его свойств, ожидается, что он будет
забракован, и планируется объявление ошибки, связанной с каждым полем. Чтобы
увидеть, проводит ли модель проверку приемлемости данных, можно воспользо­
ваться методами e r r o r s Q и i n v a l i d ? ( ) этой модели, а для того, чтобы понять,
есть ли ошибка, связанная с конкретным свойством, можно воспользоваться ме­
тодом any ? () списка ошибок.
Теперь, зная, что тестировать, нужно узнать, как сообщить среде тестирования
о том, прошел наш код проверку или нет. Это делается с помощью утверждений.
Утверждение —это обычный вызов метода, в котором среде тестирования сообща­
ется, что именно для нас является истиной.
Простейшим утверждением является метод a s s e r t , который выдвигает
предположение, что его аргументы должны быть истинными. Если это так, ниче­
го особенного не происходит. Но если аргументы у a s s e r t ложные, утверждение
не выполняется. Среда тестирования выведет сообщение и остановит выпол­
нение метода тестирования, содержащего ошибку. В нашем случае ожидается,
что незаполненная модель P r o d u c t не пройдет проверку, и это можно выразить
в виде утверждения, что товар действительно не пройдет проверку на приемле­
мость.
asse n t ! p ro d u c t.v a lid ?

Замените тест t h e t r u t h следующим кодом:


104 Часть И • Создание приложения

ra ils3 1 /d e p o t_ c/te st/u n it/p ro d u ct_ te st. rb


t e s t "p ro d u ct a t t r ib u t e s must not be empty" do
# свойства товара не должны оставаться пустыми
product = Product.new
a s s e rt p ro d u c t. in v a l id ?
a s s e rt p ro d u c t. e r r o r s [ : t i t l e ] . any?
a s s e rt p r o d u c t . e r r o r s [ d e s c r ip t io n ] .a n y ?
a s s e rt p ro d u c t. e r r o r s [ : p r i c e ] . an y?
a s s e rt p ro d u c t. e r r o r s [ : im a g e _ u rl].a n y ?

Перезапустить только блочные тесты можно с помощью команды г а к е


t e s t : u n i t s . Если это сделать, будет видно, что на этот раз тестирование выпол­
нено успешно:
depot> rake t e s t : u n i t s
Loaded s u it e lib / r a k e / r a k e _ te s t _ lo a d e r
S ta rte d

P ro d u c tT e s t:
PASS product a t t r ib u t e s must not be empty (0 .2 3 s )

F in ish e d in 0.231576 seconds.

1 te s ts , 5 a s s e r t io n s , 0 f a i l u r e s , 0 e r r o r s , 0 sk ip s

Вполне очевидно, что проверка приемлемости данных включена и все утверж­


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

ra ils3 1 /d e p o t_ c/te st/u n it/p ro d u ct_ te st. rb


t e s t "p ro du ct p r ic e must be p o s it iv e " do
# цена товара должна быть положительной
product = P ro d u c t. n e w ( t i t l e : "My Book T i t l e " ,
d e s c r ip tio n : " y y y " ,
im ag e _u rl: " z z z .jp g " )
p ro d u c t. p r ic e = -1
a s s e rt p ro d u c t. in v a l id ?
a s s e rt_ e q u a l "must be g r e a te r than or equal to 0 .0 1 ",
p r o d u c t.e r r o r s [ : p r i c e ] . j o i n ( ' ; ' )
# должна быть больше или равна 0.01
p ro d u c t. p r ic e = 0
a s s e rt p ro d u c t. in v a l id ?
a s s e rt_ e q u a l "must be g r e a te r than o r equal to 0 .0 1 ",
p r o d u c t.e r r o r s [ : p r i c e ] . j o i n ( ' ; ' )
p ro d u c t. p r ic e = 1
a s s e rt p ro d u c t. v a li d ?
end
Глава 7 • Задача Б: проверка приемлемости данных и блочное тестирование 105

В этом коде мы создаем новый товар, а затем пытаемся установить для него
цену -1 ,0 и +1, каждый раз проверяя его при этом. Если наша модель работает, то
первые две проверки должны провалиться и мы проверяем появление ожидаемого
нами сообщения об ошибке, связанного с ценой. Поскольку список сообщений об
ошибках является массивом, для объединения каждого сообщения мы воспользу­
емся подходящим методом j o i n 1 и выразим утверждение таким образом, чтобы
проверить, что есть только одно такое сообщение.
Последняя цена является приемлемой, поэтому мы утверждаем, что теперь
модель допустима. (Возможно, кто-то поместил бы эти три теста в три отдельных
тестовых метода, и это было бы вполне разумно.)
Затем протестируем проверку окончания URL-адресов изображений одним из
допустимых расширений: .gif, .jpg или .png:

ra ils3 1 /d ep o t_ e/te st/ u n it/ p ro d u ct_ te st. rb


d ef n ew _p ro d u ct(im ag e_u rl)
Pro d u ct.n ew ( t i t l e : "My Book T i t l e " ,
d e s c r ip tio n : "УУУ")
p r ic e : 1,
im ag e_u rl: im a g e _u rl)
end

t e s t "image u r l " do
# u r l изображения
ok = %w{ f r e d . g i f f r e d .jp g fre d .p n g FRED.1PG FRED.Dpg
h tt p :/ / a .b .c / x / y / z / f r e d .g if }
bad = %w{ fre d .d o c fr e d .g if/ m o re fr e d .g if.m o r e }

ok.each do |name|
a s s e rt n e w _p ro d u c t(n a m e ).v a lid ?, "#{nam e} s h o u ld n 't be in v a l id "
# не должно быть неприемлемым
end

bad.each do |name|
a s s e r t new _product(nam e). in v a l i d ? , "#{nam e} s h o u ld n 't be v a li d "
# не должно быть приемлемым
end

Здесь допущена небольшая мешанина. Вместо написания девяти отдельных


тестов используется два цикла: один —для проверки вариантов, которые предполо­
жительно пройдут проверку на приемлемость, а второй —для проверки вариантов,
которые предположительно не пройдут эту проверку. А общий код для этих двух
циклов вынесен за их пределы.
Обратите внимание, что к вызову нашего метода a s s e n t добавлен еще один
аргумент. Все тестовые утверждения принимают еще один замыкающий аргумент,
содержащий строку, которая будет записана вместе с сообщением об ошибке, если
утверждение не подтвердится, и будет полезной при диагностике нештатной си­
туации.

1 h ttp ://ru b y-d o c.O rg /co re /classe s/A rray .h tm l# M 0 0 2 1 8 2


106 Часть II • Создание приложения

И наконец, наша модель содержит проверку на уникальность всех названий то­


варов, хранящихся в базе данных. Чтобы протестировать эту проверку, нам нужно
сохранить данные о товарах в базе данных.
Можно было бы воспользоваться таким тестом, который создает товар, сохра­
няет его, затем создает еще один товар с таким же названием и пытается его сохра­
нить. Понятно, что такой тест был бы работоспособен. Но есть куда более простой
способ — воспользоваться испытательными стендами Rails.

Испытательные стенды
В мире испытаний стенды являются той средой, в которой можно запустить те­
стирование. Если, к примеру, тестируется монтажная плата, можно установить ее
на испытательном стенде, который подает на нее питание и входные сигналы, не­
обходимые для управления тестируемой функции.
В мире Rails испытательный стенд — это просто спецификация исходного со­
держимого тестируемой модели (или моделей). Если, к примеру, мы хотим обе­
спечить, чтобы перед каждым блочным тестированием в начале таблицы товаров
products хранились известные нам данные, мы можем определить это содержимое
в испытательном стенде, a Rails позаботится обо всем остальном.
Данные стенда определяются в файлах каталога test/fixtures. В этих файлах дан­
ные содержатся либо в формате CSV' (Comma-Separated Value, то есть значения,
разделенные запятыми), либо в формате YAML. Для наших тестов предпочтитель­
нее формат YAML. Каждый стендовый файл формата YAML содержит данные для
одной модели. Имя стендового файла должно совпадать с именем таблицы базы
данных. Поскольку нам нужны некоторые данные для модели Product, размещен­
ные в таблице товаров, мы впишем их в файл, названный products.yml.
Rails уже создала этот стендовый файл одновременно с созданием модели:

rails31/depot_b/test/fixtures/products. yml
# Read about fixtures a t h t t p : / / a p i. r u b y o n r a ils . o r g / c la s s e s / F ix t u r e s . html

t i t l e : M yStrin g
d e s c r ip tio n : MyText
im ag e _u rl: M yStrin g
p r ic e : 9.99

two:
t i t l e : M yStrin g
d e s c r ip tio n : MyText
im ag e _u rl: M yStrin g
p r ic e : 9.99

Стендовый файл содержит запись для каждой строки, которую нужно вста­
вить в базу данных. Каждой строке дается имя. В стенде, сгенерированном Rails,
строки названы one и two. Для базы данных эти имена ничего не значат — они
не вставляются в строки данных. Но вскоре будет показано, что имена дают нам
удобный способ ссылки на тестовые данные внутри кода нашего теста. Эти имена
Глава 7 • Задача Б: проверка приемлемости данных и блочное тестирование 107

используются также в сгенерированных комплексных тестах, поэтому пока мы их


касаться не будем.

Д ЭВИД ГОВОРИТ: ВЫ БИРАЙТЕ ПОНЯТНЫ Е СТЕНДОВЫ Е И М Е Н А --------------------


Чаще всего нам хочется, чтобы имена переменных по возможности говорили сами за себя,
то же самое относится и к стендовым данным. Тесты намного удобнее читаются, когда вы
строите утверждение вида product(:valid_order_for_fred), и это без сомнения воспринимает­
ся как правильный заказ для Фреда. К тому же намного прощ е запомнить, какой стенд вы
хотите протестировать, не выискивая названия вроде p i или order4. Чем больш е создано
стендов, тем важнее дать им всем вполне понятные имена. Поэтому старайтесь с самого
начала закладывать ф ундамент своего будущ его успеха.
А что ж е делать со стендами, которым не так-то просто присвоить смы словы е имена вро­
де v a lid „o rd e r„fo r_ fre d ? П одберите естественное название, которое ближ е всего подходит
к их роли. Н апример, вм есто того, чтобы использовать o rd e rl, воспользуйтесь именем
christm as_order. Вместо c u sto m e rl используйте fred. Как только у вас вы работается при­
вычка давать естественны е имена, вскоре вы уже начнете сочинять небольш ой рассказ
о том, как Ф ред расплачивается за свой рож дественский заказ (christm as_order) сначала
неправильной кредитной картой (invaiid_credit_card), а затем правильной кредитной кар­
той (valid_ cred it„card) и в заверш ение вы бирает отправку всего заказа своей тете Мэри
(aunt_m ary).
Истории, основанные на ассоциациях, являю тся ключом к запоминанию целого мира стен­
довы х данных.

Внутри каждой записи вы увидите оформленный с отступом список, состоящий


из пар имя-значение. Точно так же, как и в вашем файле config/database.ymi, в начале
каждой из строк данных можно использовать пробелы, но не символы табуляции,
и все строчки, формирующие строку базы данных, должны иметь одинаковый от­
ступ.
При внесении изменений нужно проявлять особую осторожность, поскольку
необходимо обеспечить, чтобы в каждой записи использовались правильные имена
столбцов —расхождение с именем столбца базы данных может привести к трудно
отслеживаемому исключению.
Добавим к стендовому файлу данные, которые можно использовать при тести­
ровании нашей модели Product:

rails31/depot_c/test/fixtures/products.ym l
ruby:
t i t l e : Programming Ruby 1.9
d e s c r ip t io n :
Ruby i s th e f a s t e s t growing and most e x c itin g dynamic
language out th e r e . I f you need to get working programs
d e liv e re d f a s t , you should add Ruby to your to o lb o x ,
p r ic e : 49.50
im ag e _u rl: ruby.png

При наличии стендового файла нужно, чтобы при запуске блочного теста среда
Rails загружала тестовые данные в таблицу p ro d u c ts . И Rails уже так и делает
(тут опять для успеха всей нашей работы соглашение превалирует над настройкой
108 Часть И • Создание приложения

конфигурации!), но какой из стендовых файлов нужно загружать, можно опреде­


лить с помощью следующей строки в файле test/unit/product_test.rb:
c la s s Pro d u ctT est < A c tiv e S u p p o rt: :TestCase
► fixtures : products
#. ..
end

Инструкция f i x t u r e s ( ) приводит к загрузке стендовых данных, соответ­


ствующих заданному имени модели в соответствующей таблице базы данных.
Эти данные загружаются прежде, чем при тестировании будет запущен каждый
тестовый метод. Имя стендового файла определяет загружаемую таблицу, поэто­
му указание : p r o d u c t s приведет к тому, что будет использован стендовый файл
products.yml.
Выразим эту мысль иным способом. В случае использования нашего класса
P r o d u c t T e s t инструкция f i x t u r e s означает, что таблица p r o d u c t s перед запу­
ском каждого тестового метода будет очищена, а затем заполнена тремя строками,
определенными в стендовом файле.
Следует заметить, что большинство временных платформ, генерируемых Rails,
не содержит вызовов метода f i x t u r e s . Причина в том, что при тестировании по
умолчанию перед запуском теста загружаются все стендовые данные. Поскольку
обычно такое поведение по умолчанию вас вполне устраивает, ничего не нужно
менять. И снова соглашение используется для избавления от ненужных настроек
конфигурации.
Метод p r o d u c t s () указывает на таблицу, созданную при загрузке стендовых
данных. Нам нужно изменить указатель, чтобы он соответствовал тому имени,
которое мы дали самому стенду.
До сих пор мы работали только в разработочной базе данных. Но теперь, когда
мы запускаем тесты, среде Rails нужно использовать тестовую базу данных. Если
посмотреть на содержимое файла database.yml в каталоге config, можно увидеть, что
Rails уже создала конфигурацию для трех отдельных баз данных:
О db/development.sqlite3 будет нашей разработочной базой данных. В ней будет
вестись вся наша работа по разработке программы;
О db/test.sqlite3 — тестовая база данных;
О db/production.sqlite3 — база данных готового изделия. Эта база будет исполь­
зоваться нашим приложением, когда оно будет запущено в сетевой среде.
Каждый тестовый метод получает заново ироинициализированную таблицу
в тестовой базе данных, данные в которую загружаются из предоставленных нами
стендовых файлов. Это происходит автоматически при запуске команды r a k e t e s t ,
но может быть сделано и отдельно, путем запуска команды r a k e d b : t e s t : pre pare .

Использование стендовых данных


Теперь, когда стало известно, как заполучить стендовые данные в базу данных,
нужно понять, как их использовать в тестах.
Глава 7 • Задача Б: проверка приемлемости данных и блочное тестирование 109

Понятно, что одним из способов чтения данных будет применение методов


поиска в модели. Но Rails предлагает более легкий способ. Для каждого загру­
жаемого в тест стенда она определяет метод с тем же именем. Вы можете исполь­
зовать этот метод для доступа к уже загруженным объектам модели, содержащим
стендовые данные: стоит только передать ему имя записи в том виде, как оно
определено в стендовом файле YAML-формата, и будет возвращен объект моде­
ли, содержащий данные этой записи. В случае с нашими данными о товаре вызов
p r o d u c t s ( :гиЬу)вернет модель P r o d u c t , содержащую данные, определенные
в стенде. Воспользуемся всем этим, чтобы протестировать проверку уникаль­
ности названий товаров.

ra ils3 1 /d ep o t_ c/te st/u n it/p ro d u ct_ test. rb


t e s t "p ro d u ct i s not v a li d w ith o u t a unique t i t l e " do
# если у товара нет уникального названия, то он недопустим
product = P r o d u c t .n e w (t it le : p ro d u c ts (: r u b y ) . t i t l e ,
d e s c r ip tio n : "УУУ"»
p r ic e : 1,
im ag e _u rl: " f r e d .g if " )
a s s e r t ! p ro d u ct.sav e
a s s e rt_ e q u a l "has a lre a d y been ta k e n ", p r o d u c t . e r r o r s [ : t i t l e ] . j o i n ( ' ; ')
# уже было использовано
end

В тесте предполагается, что база данных уже включает строку для книги по
Ruby. Он берет название ( t i t l e ) из этой уже существующей строки, используя
следующий код:
p ro d u c ts (: r u b y ) . t i t l e

Затем он создает новую модель P r o d u c t , устанавливая для нее уже существую­


щее название. Тест утверждает, что попытка сохранить эту модель будет неудачной
и что свойство t i t l e имеет соответствующую связанную с ним ошибку.
Если для сообщения об ошибке Active Record использовать жестко заданную
строку не хочется, можно подобрать соответствующее сообщение из встроенной
таблицы ошибок:

ra ils3 1 /d e p o t_ c/te st/u n it/p ro d u ct_ te st. rb


t e s t " product i s not v a lid w ith o u t a unique t i t l e - il 8 n " do
product = P r o d u c t .n e w (t it le : p r o d u c ts (: r u b y ) . t i t l e ,
d e s c r ip tio n : " y y y " ,
p r ic e : 1,
im ag e_u rl: "f r e d .g if” )
a s s e r t ! p ro d u c t. save
assert_equal I 1 8 n , t r a n s la t e ( ' a c t iv e r e c o r d .errors.m essages.taken'),
p ro d u c t.e rr o rs [ :t i t l e ] .j o i n ( '; ')

Функция I 1 8 n будет рассмотрена в главе 15 «Задача К: интернационализа­


ция».
110 Часть II • Создание приложения

Теперь мы можем быть уверены, что наш код проверки приемлемости не только
работает, но и будет работать в дальнейшем. У нашего товара теперь есть модель,
набор представлений, контроллер и набор блочных тестов. Это послужит хорошей
основой, на которой можно будет построить все остальное приложение.

7.3. Наши достижения


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

Чем заняться на досуге


Попробуйте проделать все это без посторонней помощи:
О Если вы пользуетесь системой Git, то сейчас, возможно, настал подходящий
момент для фиксации нашей работы. Сначала можно посмотреть, какие фай­
лы были изменены, воспользовавшись для этого командой g i t s t a t u s :
depot> g i t s ta tu s
# On branch master
# Changes not staged f o r commit:
# (use " g i t add < file > ..." to update what w i l l be committed)
# (use " g i t checkout -- < file > ..." to d is c a rd changes in working d ir e c t o r y )
#
# modified: app/m odels/product. rb
# modified: te s t/ fix tu re s/ p ro d u c ts.y m l
# modified: t e s t / f u n c t io n a l/ p r o d u c t s _ c o n t r o lle r _ t e s t . rb
# modified: t e s t / u n it / p r o d u c t _ t e s t . rb
# no changes added to commit (use " g i t add" and/or " g i t commit - a ")

Поскольку мы внесли изменения только в некоторые уже существую­


щие файлы и не добавили никаких новых файлов, мы можем объединить
Глава 7 • Задача Б: проверка приемлемости данных и блочное тестирование 111

команды g i t add и g i t commit и выдать единую команду g i t commit


с ключом - а:
depot> g i t commit -a -m 'V a l i d a t i o n ! '

Как только это будет сделано, мы можем делать что угодно, зная, что в случае
отказа все в любой момент можно будет вернуть к сохраненному состоянию,
используя всего лишь одну команду g i t checkout ..
О Ключ проверки приемлемости : le n g th проверяет длину свойства модели.
Добавьте эту проверку приемлемости к модели Product, чтобы проверить,
что в названии товара присутствует не менее 10 символов.
О Измените сообщение об ошибке, связанное с одной из ваших проверок при­
емлемости данных.
(П одсказки можно найти по адресу http://www.pragprog.com/wikis/wiki/
RailsPlayTime.)
Задача В: отображение
каталога товаров

Основные темы:
> создание своих собственных представлений;
> использование элементов разметки для улучшения внешнего вида страниц;
> включение CSS;
> использование помощников;
> создание функциональных тестов.

Пока в основном все у нас шло успешно. Были собраны начальные требования
заказчика, отображены на бумаге основные потоки данных, выработан первый
подход к необходимым данным и собрана страница для ведения перечня товаров
интернет-магазина. У нас даже есть небольшой, но наращиваемый набор тестов.
И мы с воодушевлением переходим к решению следующей задачи. При об­
суждении приоритетов с заказчиком он высказал пожелание увидеть приложение
глазами покупателя. Поэтому следующей задачей будет создание простого ото­
бражения каталога товаров.
Мы также считаем это желание вполне обоснованным. Раз у нас уже есть това­
ры, помещенные в базу данных после соответствующей проверки, вывести их на
экран особого труда не составит. Тем самым будут заложены основы для дальней­
шей разработки кода корзины покупателя.
Нужно будет также воспользоваться уже проделанной работой по ведению
перечня товаров, ведь, по сути дела, отображение каталога товаров — не что иное,
как вывод их привлекательно оформленного перечня.
И наконец, нам также нужно будет дополнить наши блочные тесты для модели
несколькими функциональными тестами для контроллера.
Глава 8 • Задача В: отображение каталога товаров 113

8.1. Шаг В1: создание каталога товаров


У нас уже создан контроллер товаров, используемый продавцом для управления
приложением Depot. Настало время создать второй контроллер, который будет
работать с покупателями. Назовем его S to re .
depot> r a i l s generate c o n tr o lle r Store index
c re a te a p p / c o n t r o lle r s / s t o r e _ c o n t r o lle r . rb
ro u te get "s to re / in d e x "
invoke erb
c re a te ap p /view s/sto re
c re a te a p p /vie w s/ sto re /in d e x . h tm l. erb
invoke t e s t _ u n it
c re a te t e s t / f u n c t io n a l / s t o r e _ c o n t r o ll e r _ t e s t . rb
invoke h e lp e r
c re a te a p p / h e lp e rs / s to re _ h e lp e r. rb
invoke t e s t _ u n it
c re a te te s t / u n it / h e lp e r s / s t o r e _ h e lp e r _ t e s t . rb
invoke a s se ts
invoke c o ffe e
c re a te a p p / a s s e ts / ja v a s c r ip ts / s t o r e . j s . c o ffe e
invoke scss
c re a te a p p / a s s e ts / s ty le s h e e ts / s to re . c s s . scss

Как и ранее при создании контроллера и связанной с ним временной платформы


для работы с перечнем товаров, воспользуемся утилитой g e n e ra te , но теперь по­
требуем от нее создать контроллер (класс S to re C o n tro lle r в файле store_controiler.
rb), содержащий один метод действия — in d e x ( ).
Поскольку для доступа к этому действию по адресу http://localhost:ЗООО/store/
index (можете попробовать!) все настройки уже произведены, мы можем кое-что
усовершенствовать. Давайте упростим ситуацию для пользователя и сделаем
этот URL-адрес корневым для веб-сайта. Отредактируем для этого файл config/
routes, rb:
rails31/depot_d/config/routes. rb
D e p o t : A p p lic a t io n . r o u t e s . draw do
get "s to re / in d e x "

re so u rce s :products

# ...
# You can have th e root o f your s i t e routed w ith "r o o t"
# (Корневой маршрут к вашему сайту можно получить с помощью " r o o t " )
# j u s t remember to d e le te p u b lic / in d e x . h tm l.
# (не забудьте удалить файл p u b lic / in d e x .h tm l.)
► ro o t to : ' s to re # in d e x ', as: 's t o r e '
# . . .
end

В верхней части файла можно увидеть строки, добавленные для поддержки


контроллеров магазина и товаров. Эти строки мы изменять не будем. Далее в файле
114 Часть И • Создание приложения

вы увидите закомментированную строку, определяющую корневой адрес для веб­


сайта. Можно либо убрать знак комментария из этой строки, либо добавить сразу
же после нее новую строку. В этой строке будет изменено только название кон­
троллера (с welcome на s to r e ) и добавлено as : ' s t o r e '. Последнее добавление
заставит Rails создать переменную s to re _ p a th , как это было в разделе 2.3 с пере­
менной say_goodbye_path.
Обратите внимание на то, что комментарии также предписывают удалить файл
public/index.html. Давайте так и сделаем1:
depot> rm p u b lic / in d e x .h tm l

Посмотрим, что получилось. Укажем браузеру адрес http://localhost:3000/ и вы­


ведем нашу веб-страницу:

Вряд ли от этого мы станем богаче, но, по крайней мере, мы узнали, что все
связано друг с другом правильно. Страница просто сообщает о том, где найти файл
шаблона, который ее рисует.
Начнем с отображения простого перечня всех товаров, имеющихся в базе дан­
ных. Мы знаем, что в конечном счете понадобится более сложный подход с раз­
биением товаров на категории, к чему мы и будем стремиться.
Нам нужно извлечь перечень товаров из базы данных и сделать его доступным
для кода представления, который и отобразит таблиц)'. Это означает, что мы долж­
ны внести изменения в метод in d ex () в файле store_controller.rb. Мы хотим програм­
мировать на соответствующем уровне абстракции, поэтому просто предположим,
что мы можем запросить у модели перечень продаваемых товаров:

ra ils3 1 / d e p o t_ d / a p p / c o n tro lle rs/ sto re _ co n tro lle r. rb


c la s s S t o r e C o n tr o lle r < A p p lic a tio n C o n t r o lle r
d e f index
► ^ products = P r o d u c t . o r d e r ( : t i t l e )
end

end

Мы спросили у заказчика, is каком порядке он хочет увидеть товары, которые


должны быть выведены в перечне, и приняли совместное решение: посмотреть,

1 Пользователям Windows нужно воспользоваться командой erase public\index.html. Если


исходный код управляется с помощью системы Git, нужно воспользоваться командой
git rm public/index.html.
Глава 8 • Задача В: отображение каталога товаров 115

что получится, если мы отобразим их в алфавитном порядке. Это делается


путем добавления вызова метода o r d e r ( : t i t l e ) () применительно к модели
Product.
Теперь нужно создать шаблон представления. Для этого отредактируем файл
index.html.erb в каталоге app/views/store. (Вспомним, что путевое имя к представ­
лению выстраивается из имени контроллера [ s t o r e ] и имени действия [in d e x ].
Часть имени .html.erb означает, что это ERB-шаблон, генерирующий HTML.)
rails31/depot_d/app/view s/store/index. htm l. erb
<% i f n o tic e %>
<p id="notice"><%= n o tic e %></p>
<% end %>

<hl>Your Pragm atic Catalog</hl>

<% ^ p ro d u c ts. each do |product| %>


<div c la s s= "e n try ">
<%= im ag e _tag (p ro d u ct. im a g e _u rl) %>
<h3><%= p r o d u c t . t i t le %></h3>
<%= s a n it iz e (p r o d u c t .d e s c r ip t io n ) %>
<div c la s s = " p r i c e _ l in e " >
<span class= "price"> < %= p ro d u c t. p r ic e %></span>
</div>
</div>
<% end %>

Обратите внимание на использование метода s a n i t i z e ( ) для описания


( d e s c r ip tio n ) . Он позволяет безопасно добавлять стилевое оформление HTML
повышающее интерес наших клиентов к описаниям. (Следует заметить, что данное
решение открывает потенциальную дыру безопасности1, но, поскольку описания
готовятся людьми, работающими в самой компании, мы полагаем, что риск мини­
мален.)
Здесь также используется вспомогательный метод im age_tag(). Он генерирует
HTM L-тег <img>, используя свой аргумент в качестве источника изображения.
Затем мы добавим таблицу стилей, воспользовавшись тем фактом, что все
настроено таким образом, что в страницах, созданных S to r e C o n tr o lle r , будет
определен HTML-атрибут c la s s по имени s to re :

ra ils3 1 /d ep o t_ d / a p p /a ssets/style sh e e ts/sto re .css. scss


// P la c e a l l the s t y le s r e la te d to th e S to re c o n t r o lle r h ere.
// (Сюда помещаются все определения стилей для контроллера S t o r e . )
// They w i l l a u t o m a tic a lly be in clud ed in a p p lic a t io n . c s s .
// (Они будут автоматически включены в файл a p p lic a t io n . c s s . )
// You can use Sass (SC SS ) he re: h ttp :// sa ss- lan g .co m /
// (Что такое Sass (SC SS ) можно узн ать здесь: h ttp :/ / s a s s - la n g .c o m / )

► .s to re {
► hi {

1 h ttp ://w w w .o w a sp .o rg /in d e x .p h p /C ro ss-site _ S crip tin g _ % 2 8 X S S % 2 9


116 Часть II • Создание приложения

m argin: 0;
padding-bottom: 0.5em;
fo n t : 150% s a n s - s e r if ;
c o lo r : #226;
border-bottom : 3px dotted #77d;
}
/* Запись в каталоге s to re */
.e n t r y {
overflow: auto;
m argin-top: lem;
border-bottom : lp x dotted #77d;
m in-height: 100px;
img {
w id th : 80px;
m a rg in - rig h t: 5px;
margin-bottom: 5px;
p o s itio n : a b s o lu te ;
}
h3 {
fo n t - s iz e : 120%;
fo n t- fa m ily : s a n s - s e r if ;
m a rg in - le ft: 100px;
m argin-top: 0;
margin-bottom: 2px;
c o lo r : #227;
}
p, d i v . p r i c e _ l i n e {
m a rg in - le ft: 100px;
m argin-top: 0.5em;
margin-bottom: 0.8em;
}
.p r ic e {
c o lo r : #44a;
fo n t- w e ig h t: bold;
m a rg in - rig h t: 3em;
}
}

Щелчок на кнопке Обновить (Refresh) приведет к появлению картинки, показан­


ной на рис. 8.1. Она все еще не отличается особой привлекательностью... чего-то
в ней не хватает. Как раз во время наших размышлений мимо проходил заказчик,
который выразил пожелание, чтобы на общедоступных страницах был привлека­
тельный заголовок и боковая панель.
Если бы это все происходило на самом деле, нам, наверное, захотелось бы об­
ратиться к дизайнеру. Так как всем нам попадалось на глаза слишком большое
количество веб-сайтов, дизайн которых разрабатывался самими программистами,
мы без малейших колебаний привлекли бы к этому делу кого-нибудь другого. Но
наш веб-дизайнер в отпуске, черпает вдохновение на каком-нибудь пляже и вряд ли
вернется до конца года, поэтому пока в этой графе поставим прочерк. А тем време­
нем пришла пора сделать следующий шаг.
Глава 8 • Задача В: отображение каталога товаров 117

locathost ☆i \
Your Pragmatic Catalog

C offe e S crip t
Co ffe e Scrip t is Ja va S crip t done right. 51 provides all of
J a va S c rip t's functionality wrapped in a cleaner more
su c c in c t syntax. In the first hook on this exciting new
language. CoffeeScrip t guru Trevor Burnham show s you
how to hold onto ail the power and flexibility of Ja va S crip t
w hile writing clearer cleaner, and safer code.

36.0

Рис. 8.1. Наша первая (черновая) страница каталога

8.2. Шаг В2: добавление макета страницы


Страницами типового веб-сайта часто используется один и тот же макет, поэто­
му разработчик должен создать стандартный шаблон, применяемый при разме­
щении содержимого. Наша задача состоит в изменении данной страницы с целью
добавления привлекательности каждой странице интернет-магазина.
До сих пор мы вносили лишь незначительные изменения в файл appiication.html.
erb, в частности, для добавления атрибута c la s s в шаге А2. Поскольку этот файл
является макетом, используемым всеми представлениями всех контроллеров, ко­
торые не предоставили свой собственный макет, мы можем изменить внешний вид
всего сайта, отредактировав всего один файл. Это позволяет нам пока поставить
в графе макета страницы прочерк, который можно будет убрать, как только наш
дизайнер наконец-то вернется из отпуска на островах.
Давайте обновим этот файл, чтобы определить заголовок и боковую панель:

ra ils31/depot_e/app/view s/layou ts/application. htm l. erb


Строка 1 <!DOCTYPE html>
<html>
<head>
< title > P rag p ro g Books O n lin e S to re < / title >
<%= s t y le s h e e t _ lin k _ t a g " a p p lic a t io n " %>
<%= ja v a s c r ip t _ in c lu d e _ t a g " a p p lic a t io n " %>
<%= c srf_m e ta _ta g %>
</head>
<body class="<%= c o n tr o lle r .c o n t r o lle r _ n a m e %>">
10 <div id="banner">
<%= im a g e _ ta g ("lo g o .p n g ") %>
<%= (3 p a g e _ title | | "P rag m a tic B o o k s h e lf" %>
</div>
<div id="columns">
15 <div id = "sid e">
118 Часть II • Создание приложения

<ul>
< lix a h re f= "http://www .. .. ">Home</ax/li>
< lix a h re f= "h t t p : //www . . . ./ fa q "> Q u e stio n s< / a x / li>
< lix a h ref= ” h t t p : //www . . . ./news">New s</ax/li>
20 < lix a hnef= "h t t p : //www . . . ./ c o n ta c t"> C o n ta c t< / a x / li>
</ul>
</div>
<div id="main">
<%= y i e ld %>
25 </div>
</div>
- </body>
- </html>

Кроме обычного HTML-кода этот макет содержит 3 характерных для Rails эле­
мента. Как уже ранее упоминалось, в строке 5 используется вспомогательный метод
Rails, предназначенный для генерирования тега <link>, ссылающегося на таблицу
стилей нашего приложения. Точно так же в строке 6 генерируется тег <link>, ссы­
лающийся на сценарии нашего приложения. И наконец, в строке 7 устанавливаются
все закулисные данные, необходимые для предотвращения атак путем подделки
кросс-сайтовых запросов, которые выйдут на первый план, как только мы добавим
формы в главе 12 «Задача Ж: оформление покупки».
В строке 12 для заголовка страницы установлено значение переменной экзем­
пляра @ p a g e _ title . Но настоящие чудеса начинаются в строке 24. При вызове
действия y ie ld Rails автоматически подставляет содержимое, специфическое для
текущей страницы, то есть все, что сгенерировано представлением, вызванным
данным запросом. В данном случае это будет страница каталога, сгенерированная
с использованием файла index.htmi.erb.
Чтобы все это заработало, сначала переименуем файл application.css в application,
css.scss, а затем добавим к нему следующее содержимое:

r a i ls 3 1 / d e p o t _ e / a p p / a s s e t s / s t y l e s h e e t s / a p p l i c a t i o n . c s s .s c s s
/*
* T h is i s a m an ifest file t h a t ' l l a u to m a tic a lly in c lu d e a l l the
* s t y le s h e e ts a v a ila b le in t h i s d ir e c t o r y and any s u b - d ire c to r ie s .
* You’ re f r e e to add a p p lic a tio n - w id e s t y le s to t h i s file and t h e y ' l l
* appear a t th e top o f th e compiled file , but i t ' s g e n e r a lly b e tte r
* to c re a te a new file per s t y le scope.
*= r e q u ir e _ s e lf
*= r e q u ir e jt r e e .
*/
#banner {
background: #9c9;
padding: 10px;
border-bottom : 2px s o lid ;
fo n t : sm all-caps 40px/40px ’’Times Mew Roman” , s e r i f ;
c o lo r : #282;
t e x t - a lig n : c e n te r;

img {
Глава 8 • Задача В: отображение каталога товаров 119

flo at: l e f t ;
}
}
# n o tic e {
c o lo r : #000 lim p o rta n t;
b ord er: 2px s o lid red;
padding: lem;
m argin-bottom: 2em;
b ackground-color: # f0 f0 f0 ;
fo n t : bold s m a lle r s a n s - s e r if ;

#columns {
background: #141;

#main {
m a rg in - le ft: 17em;
padding: lem;
background: w h ite ;
}
# sid e {
flo at: l e f t ;
padding: lem 2em;
w id th : 13em;
background: #141;

ul {
padding: 0;

H {
lis t - s ty le : none;

a {
c o lo r : #bfb;
fo n t - s iz e : sm a ll;
}
}
}
}
}

Как объяснено в комментариях, этот файл-манифест автоматически включит


все таблицы стилей, доступные в данном каталоге и в любом из его подкаталогов.
Это действие выполняется благодаря инструкции require__tree.
Вместо этого можно перечислить имена отдельных таблиц стилей, на которые
нам нужна ссылка в s ty le s h e e t_ lin k _ ta g ( ), но, поскольку мы находимся в маке­
те всего приложения и поскольку этот макет уже настроен на загрузку всех таблиц
стилей, пока оставим все без изменений.
И опять мы можем в полной мере воспользоваться Sass, такую возможность дает
нам проведенное переименование файла. Например, в данном файле используется
120 Часть II • Создание приложения

селектор img, вложенный в селектор #banner. Имеется также селектор, вложенный


в селектор # sid e .
Щелчок на кнопке Обновить (Refresh) приведет к появлению картинки, пока­
занной на рис. 8.2. Конечно, призов на конкурсе дизайна наш каталог не завоюет,
но заказчик получит примерное представление о том, каким будет окончательный
вид страницы.

io c a ih o s t ☆ Л

P r a g m a t ic B o o k s h e l f
Your Pragmatic Catalog

CoffeeScript
C offeeScrip t is Ja va S crip t done right, ft provides all of
J a va S c rip t's functionality wrapped in a cleaner, more
su c c in c t syntax. In the first book on this exciting пел?
language. C offeeScript guru Trevor Burnham show s you
how to hold onto ail the power and flexibility of
J a va S c rip t w hile writing clearer, cleaner, and safer code.

36.0

Рис. 8.2. Каталог с добавленным макетом

Рассматривая эту страницу, мы заметили небольшую проблему с отображением


цены товара. База данных хранит цену в виде числа, но нам хотелось бы показать
ее в долларах и центах. Цена 12.34 должна быть показана в виде $12.34, а цена 13 —
в виде $13.00. Именно этим мы сейчас и займемся.

8.3. Шаг ВЗ: использование помощника


для форматирования цены
Ruby предоставляет функцию s p r i n t f ( ) , которая может использоваться для
форматирования цен. Мы можем поместить логику, использующую эту функцию,
непосредственно в представление. Например, можно воспользоваться следующим
кодом:
<span class= "price"> < %= s p r in t f (" $ % 0 . 0 2 f " , p ro d u c t. p r ic e ) %></span>

Данный код будет работать, но он вставляет все сведения о текущем формати­


ровании в представление. Если потребуется показать цену товаров в нескольких
местах или чуть позже появится желание интернационализировать приложение,
возникнут проблемы с его обслуживанием.
Глава 8 • Задача В: отображение каталога товаров 121

Давайте вместо этого воспользуемся вспомогательным методом для придания


цене формата валюты. В Rails есть подходящий для этого встроенный помощник
под названием n u m b e r_ to _ c u rre n c y ().
Этот помощник легко встраивается в представление, для этого нужно лишь
в шаблоне index поменять этот код:
<span class= "price"> < %= p ro d u c t. p r ic e %></span>

на следующий:

rails31/depot_e/app/view s/store/index. htm l. erb


<span class= "price"> < %= n u m b er_to _cu rren cy(p ro d u ct. p r ic e ) %></span>

Я уверен, что после щелчка на кнопке Обновить (Refresh) появится красиво от­
форматированная цена, показанная на рис, 8.3.

j_j Pragprog Books Online S

у- \ lo c a ih o s t 3000

P r a g m a t ic B o o k s h e l f
Your Pragmatic Catalog
Q uestions
News
ШШ C o ffe e S c rip t
Contact CoffeeScrip t is Ja v a S c rip t done right, it provides all of
J a va S c rip t's functionality wrapped in a cleaner more
su c c in c t syntax. In the first hook on this exciting new
language. C o ffeeScrip t guru Trevor Burnham show s you
how to hold onto all the power and flexibility of
Ja v a S c rip t while writing clearer, cleaner and safer code.

$ 36.00

Рис. 8.3. Каталог с отформатированной ценой

Вроде бы все выглядит вполне прилично, но у нас возникает ноющее чувство,


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

8.4. Шаг В4: функциональное


тестирование контроллеров
Теперь настал момент истины. Прежде чем сконцентрироваться на написании но­
вых тестов, нам нужно определить, не было ли нами что-либо испорчено. Памятуя
122 Часть II • Создание приложения

о нашем опыте работы после добавления к нашей модели логики проверки при­
емлемости данных, мы с чувством тревоги опять запустили наши тесты:
depot> rake test
На сей раз все прошло успешно. Несмотря на довольно многочисленные до­
бавления, мы ничего не испортили. Можно облегченно вздохнуть, но наша работа
на этом не закончена, нам все равно нужны тесты для всего, что только что было
добавлено.
Ранее проведенное блочное тестирование моделей представлялось вполне до­
статочным. Мы вызывали метод и сравнивали возвращенные им данные с тем, что
ожидали от него получить. Но теперь мы имеем дело с сервером, обрабатывающим
запросы, и с пользователем, который просматривает ответы в браузере. Нам нужны
функциональные тесты, проверяющие совместную работу модели, представления
и контроллера. Но нам не о чем особо беспокоиться —среда Rails и здесь облегчила
нашу задачу.
Сначала посмотрим на тот код, который Rails для нас сгенерировала:

ra ils3 1 / d e p o t_ d / te s t/ fu n c tio n a l/ sto re _ c o n tro lle r_ te s t. rb


re q u ire ' t e s t _ h e lp e r '
c la s s S to re C o n tro lle rT e s t < A c tio n C o n tro lle r::T e s tC a s e
t e s t " s h o u l d g e t in d e x " do
g e t : in d e x
a s se rt_ re sp o n se : success
end
end

Тест «should get index» получает индекс и утверждает, что ожидается успешный
ответ. Все это выглядит предельно просто. Для начала неплохо, но нам еще нужно
проверить, содержит ли ответ наш макет, нашу информацию о товаре и наше чис­
ловое форматирование. Посмотрим, как это выглядит в виде программного кода:

ra ils3 1 / d e p o t_ e / te st/ fu n c tio n a l/ s to re _ c o n tro lle r_ te st.rb


re q u ire 't e s t _ h e lp e r '
c la s s S to re C o n tro lle rT e s t < A c tio n C o n tro lle r::T e s tC a s e
t e s t " s h o u l d g e t in d e x " do
g e t : in d e x
a s se rt_ re sp o n se : success
► a s s e r t _ s e l e c t '#colum ns # s id e a ' , minimum: 4
► a s s e r t _ s e l e c t '#main . e n t r y ' , 3
► a s s e r t _ s e l e c t ' h 3 ' , 'P ro g ra m m in g Ruby 1 . 9 '
► a s s e r t_ s e le c t ' . p r i c e ' , / \ $ [ , \d ]+ \. \d\d/
end
end

Четыре добавленные нами строки кода заглядывают в HTML-код, который воз­


вращается при использовании нотации селекторов CSS. Следует напомнить, что
селекторы, начинающиеся с символа решетки (#), соответствуют атрибутам id,
селекторы, начинающиеся с точки (.), соответствуют атрибутам c la ss, а селекторы
вообще без префикса соответствуют названиям элементов.
Глава 8 • Задача В: отображение каталога товаров 123

Первый тест с селектором ищет элемент по имени а, который содержится


в элементе с атрибутом id , имеющим значение s i d e , который в свою очередь со­
держится внутри элемента с атрибутом id , имеющим значение colum ns. Этот тест
проверяет, что у нас имеется минимум четыре таких элемента. Использование
метода a s s e r t _ s e l e c t () является весьма эффективным средством, не правда ли?
В следующих трех строках проводится проверка отображения всех наших
товаров. В первой строке проверяется наличие трех элементов, у которых есть
атрибут c l a s s по имени e n t r y в той части страницы, которая имеет атрибут i d со
значением main. В следующей строке проверяется наличие элемента ИЗ с названием
введенной нами ранее книги по Ruby. В третьей строке проверяется правильное
форматирование пены. Эти утверждения основаны на тестовых данных, которые
мы поместили в наши стенды:

rails31/depot_e/test/fixtures/products. yml
# Read a b o ut f i x t u r e s a t h t t p : / / a p i . r u b y o n r a i l s . o r g / c l a s s e s / F i x t u r e s . h tm l

t i t l e : M yS trin g
d e s c r i p t i o n : MyText
i m a g e _ u r l: M y S t r i n g
p r i c e : 9 .9 9

tw o:
t i t l e : M y S trin g
d e s c r i p t i o n : MyText
i m a g e _ u r l: M y S t r i n g
p r ic e : 9.99

ru by:
t i t l e : Programming Ruby 1 . 9
d e s c rip tio n :
Ruby i s t h e f a s t e s t g r o w in g and most e x c i t i n g dynamic
la n g u a g e o u t t h e r e . I f you need t o g e t w o r k in g pr ogr am s
d e l i v e r e d f a s t , you s h o u ld add Ruby t o y o u r t o o l b o x ,
p r i c e : 4 9 .5 0
im ag e _ u rl: ru by.png

Если вы заметили, тип теста, выполняемого методом a s s e r t _ s e l e c t ( ), изме­


няется на основе типа его второго аргумента. Если это число, оно будет считаться
количеством. Если это строка, она будет рассматриваться как ожидаемый результат.
Еще один полезный тип теста вызывается регулярным выражением, использован­
ным в нашем последнем утверждении. Мы проверяем наличие цены, имеющей
значение, состоящее из знака доллара, за которым следует любое число (гю крайней
мере, одно), запятые или цифры, за которыми следует десятичная точка, после
которой стоят две цифры.
Еще один заключительный нюанс: и проверка приемлемости данных, и функ­
циональные тесты будут тестировать поведение только контроллера и не будут
распространять обратное действие на объекты, уже существующие в базе данных
или в стендах. В предыдущем примере два товара содержат одно и то же название.
124 Часть II '.Создание приложения

Такие данные не вызовут проблем, и не будут обнаружены до тех пор, пока такие
записи не будут изменены или сохранены.
Мы коснулись лишь нескольких возможностей метода a s s e r t _ s e l e c t ( ).
Более подробную информацию можно найти в онлайн-документации1. Итак, мы
получили массу проверок, добавив всего лишь несколько строк кода. Убедиться
в работоспособности этого кода можно, перезапустив одни лишь функциональные
тесты (ведь, по сути, только их мы и изменяли):
depot> r a k e t e s t :f u n c t i o n a l s

Теперь у нас есть не только кое-что хорошо заметное на электронной витрине,


но и тесты, гарантирующие, что все части приложения — модель, представление
и контроллер — работают в тесном взаимодействии на выдачу желаемого резуль­
тата. Звучит, конечно, внушительно, но добиться всего этого с Rails было совсем не
трудно. По сути дела, в основном приложение состояло из HTML и CSS, и в нем
было совсем немного программного кода или тестов.

8.5. Наши достижения


Мы собрали воедино все, что составляет основу отображения каталога товаров
интернет-магазина. При этом мы поэтапно выполнили следующие действия:
0 Создали новый контроллер для работы с клиентом.
0 Реализовали используемое по умолчанию действие in d e x ( ).
0 Добавили к модели P r o d u c t метод d e f a u l t _ s c o p e ( ) для указания порядка
перечисления элементов на веб-сайте.
0 Реализовали представление (файл .html.erb) и содержащий его макет (еще
один файл .html.erb).
0 Воспользовались вспомогательным методом для нужного нам форматиро­
вания цен.
0 Воспользовались каскадной таблицей стилей (CSS).
0 Создали функциональные тесты для нашего контроллера.
Нужно все это проверить и перейти к выполнению следующей задачи, а именно
к созданию корзины покупателя!

Чем заняться на досуге


Попробуйте сделать все это без посторонней помощи:
О Добавить к боковой панели дату и время. Не нужно добиваться обновления
их показаний, достаточно показать значения на момент отображения стра­
ницы.

1 http://api.rubyonrails.org/classes/ActionDispatch/Assertions/SelectorAssertions.html
Глава 8 • Задача В: отображение каталога товаров 125

О Поэкспериментировать с установкой различных аргументов вспомогатель­


ного метода n u m b e r _ t o _ c u r r e n c y и оценить влияние этих изменений на
вывод вашего каталога товаров.
О Создать ряд функциональных тестов для ведения перечня товаров, восполь­
зовавшись для этого методом a s s e r t _ s e le c t . Тесты нужно будет поместить
в файл test/fu nction а I/рrod ucts_co nt го11er_test.гb.
О Напоминаю, что завершение выполнения задания —хороший повод для со­
хранения своей работы с использованием системы Git. И если вы следовали
всем предписаниям, на данный момент у вас уже есть все необходимое для
этого действия. Выбор объектов резервного копирования будет рассмотрен
при исследовании дополнительных функциональных возможностей системы
в подразделе «Подготовка сервера для размещения вашего приложения»,
который находится в разделе 16.2.
(Подсказки можно найти по адресу http://www.pragprog.com/wikis/wiki/RailsPlay
Time.)
Задача Г: создание
корзины покупателя

О сн о в н ы е тем ы :

> что такое сессии и как ими управлять;


> как добавить взаимодействие между моделями;
> как добавить кнопку для помещения товара в корзину.

Теперь, когда мы можем вывести каталог, содержащий все наши замечательные


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

9.1. Шаг Г1: обнаружение корзины


Просматривая наш онлайн-каталог, пользователи (как мы надеемся) выбирают по­
купаемые товары. По соглашению каждый выбранный товар будет добавлен к вирту­
альной корзине покупателя, которая поддерживается в нашем магазине. В какой-то
момент у наших покупателей будет все необходимое, и они перейдут на нашем сайте
к оформлению покупки, где они рассчитаются за товар, находящийся в их корзине.
Следовательно, наше приложение должно отслеживать все товары, добавленные
покупателем в корзину. Для этого мы будем хранить корзину в базе данных и сохра­
нять ее собственный идентификатор, c a r t . id, в сессии. При каждом поступлении
запроса мы можем восстановить идентичность из сессии и воспользоваться ею для
обнаружения корзины в базе данных.
Глава 9 • Задача Г: создание корзины покупателя 127

Давайте продолжим работу и создадим корзину:


depot> r a i l s generate sc a ffo ld c a rt

depot> rake db:migrate


== C re a te C a rts : m ig ra tin g ====================================================
-- c r e a t e _ t a b l e ( : c a r t s )
-> 0.0012s
== C re a te C a rts : m igrated (0.0 01 4s) ===========================================

Для контроллера Rails делает текущую сессию похожей на хэш, поэтому мы со­
храним идентификатор корзины в сессии путем индексирования его обозначением
:c a rt_ id .

ra ils 3 1 / d e p o t_ f/ a p p / c o n tro lle rs / a p p lic a tio n _ c o n tro lle r.rb


c la s s A p p lic a tio n C o n t r o lle r < A c t io n C o n t r o lle r :: Base
p ro te c t_ fro m _ fo rg e ry

p r iv a te

d e f c u r r e n t_ c a r t
C a rt .fin d (s e s s io n [ : c a r t _ i d ] )
rescu e A c t iv e R e c o r d :: RecordNotFound
c a r t = C a r t .c r e a t e
s e s s i o n f : c a r t _ id ] = c a r t . i d
c a rt
end
end

Метод c u r r e n t _ c a r t ( ) начинается с получения : c a n t _ i d из объекта s e s s i o n


с последующей попыткой обнаружения корзины, соответствующей данному
идентификатору. Если такая запись корзины не найдется (а это произойдет, если
идентификатор будет иметь значение, по каким-то причинам не подходящее, или
значение n i l ) , этот метод приступит к созданию нового объекта C a r t , сохранит
идентификатор созданной корзины в сессии, а затем вернет новую корзину.
Следует заметить, что мы поместили метод c u r r e n t _ c a r t ( ) в A p p l i c a t i o n ­
C o n t r o l l e r и пометили его закрытым ( p r i v a t e ) . Такая трактовка делает этот
метод доступным только контроллерам и к тому же не позволяет Rails когда-либо
делать его доступным в качестве действия контроллера.

9.2. Шаг Г2: связывание товаров


с корзинами
Мы обратились к сессиям, потому что нам нужно где-то хранить нашу корзину
покупателя. Более подробно сессии будут рассматриваться в подразделе «Сессии
Rails» раздела 20.3, а сейчас мы продолжим реализацию корзины.
Не станем ничего усложнять. Корзина содержит набор товаров. На осно­
ве схемы исходных предположений о составе данных, показанной на рис. 5.3,
128 Часть II • Создание приложения

в сочетании с краткой беседой с нашим заказчиком, мы можем приступить


к генерации моделей Rails и к заполнению миграций с целью создания соответ­
ствующих таблиц:
depot> r a i ls generate sca ffo ld lin e_item p ro d u ct_id :in teg er c a rt_ id :in te g e r

depot> rake db:migrate


== C re a te L in e lte m s : m ig ra tin g ================================================
-- c r e a t e _ t a b le (: lin e _ it e m s )
-> 0.0013s
== C re a te L in e lte m s : m igrated (0.0 01 4s) =======================================

Теперь у базы данных есть место для хранения взаимоотношений между то­
варными позициями ( l i n e item ), корзинами ( c a r t ) и товарами (p ro d u ct). А вот
в приложении Rails эти взаимоотношения не фигурируют. Нам нужно добавить
к нашей модели ряд инструкций, указывающих на их взаимосвязь.
Откройте только что созданный файл cart.rb в каталоге app/models и добавьте
вызов метода has_many():

rails31/depot_f/app/m odels/cart. rb
c la s s C art < A c t iv e R e c o r d :: Base
► has_many :lin e _ it e m s , dependent: :d e s tro y
end

Часть инструкции has_many : lin e _ ite m s говорит сама за себя: в корзине по­
тенциально имеется множество (has many) связанных с ней товарных позиций.
Они привязаны к корзине, потому что каждая товарная позиция содержит ссыл­
ку на идентификатор этой корзины. Часть инструкции d e p e n d e n t: : d e s tr o y
показывает, что существование товарной позиции зависит от существования кор­
зины. Если мы уничтожим корзину, удалив ее из базы данных, нам нужно, чтобы
Rails также уничтожила все товарные позиции, связанные с этой корзиной.
Затем мы определим связи в обратном направлении, от товарной позиции
к таблицам корзин ( c a r t s ) и товаров (p ro d u c ts). Для этого в файле linejtem.rb
мы дважды воспользуемся инструкцией b e lo n g s _ to ( ):

rails31/depot_f/app/m odels/line_item . rb
c la s s Lin e lte m < A c tiv e R e c o rd : : Base
► b elo ng s_to : product
► belo ng s_to :c a r t
end

Инструкция b e lo n g s _ to сообщает Rails, что строки в таблице товарных по­


зиций ( lin e _ ite m s ) являются дочерними по отношению к строкам в таблицах
корзин ( c a r ts ) и товаров (p ro d u c ts). Пока не будут существовать соответствую­
щие строки корзины и товара, строка товарной позиции существовать не может.
Запомнить, где именно нужно помещать инструкцию belongs_to, несложно: если
у таблицы есть внешние ключи, соответствующая модель должна иметь для каж­
дого из них инструкцию b elo ngs_to.
А что же все-таки все эти разные инструкции делают? По сути, они добавляют
к объектам модели возможности перемещения. Поскольку мы добавили к Lineltem
Глава 9 • Задача Г: создание корзины покупателя 129

инструкцию belongs_to, теперь мы можем извлечь товар (Product), соответствую­


щий позиции, и отобразить название книги:
П = L in e lte m .fin d ( . . . )
puts "Эта товарная позиция относится к # { l i . p r o d u c t . t i t l e } "

И, поскольку объявлено, что корзина (C a rt) может иметь множество товарных


позиций, мы можем сослаться на них (как на коллекцию) из объекта корзины:
c a r t = C a r t .find( . . . )
puts "Количество товарных позиций в этой корзине: # { c a r t .lin e _ it e m s .c o u n t } "

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


has_many к нашей модели товаров (P roduct). Ведь если имеется множество кор­
зин, на каждый товар может ссылаться множество товарных позиций. На этот раз
для предотвращения удаления товаров, на которые ссылаются товарные позиции,
мы воспользуемся проверочным кодом.

rails31/depot_f/app/m odels/product. rb
c la s s Produ ct < A c tiv e R e c o rd : : Base
► has_many :lin e _ ite m s

► b e fo re _ d e stro y :e n s u re _ n o t_ re fe re n c e d _ b y _ a n y _ lin e _ ite m

# ...

► p r iv a te

► # убеждаемся в отсутствии товарных позиций, ссылающихся на данный товар


► d e f e n su re _n o t_re fe re n ce d _b y _a n y _lin e _ite m
► i f lin e _ ite m s .e m p ty ?
► re tu rn tru e
► e ls e
► e r r o r s . a d d (: base, 'существуют товарные позиции')
► re tu rn f a ls e
► end
► end
end

Здесь объявляется, что у товара много товарных позиций, и определяется


подключаемый метод по имени e n s u re _ n o t_ re fe re n c e d _ b y _ a n y _ lin e _ ite m ().
Подключаемым называется такой метод, который Rails вызывает автоматически в
определенный момент жизни объекта. В данном случае метод будет вызван перед
тем, как Rails попытается удалить строку в базе данных. Если подключаемый метод
возвращает f a ls e , строка не будет удалена.
Заметьте, что у нас есть непосредственный доступ к объекту e r r o r s . Это то же
самое место, где v a li d a te s () хранит сообщения об ошибках. Ошибки могут быть
связаны с отдельными свойствами, но в данном случае мы связали ошибку с самим
базовым объектом.
Более подробная информация о взаимодействиях внутри модели будет дана
в подразделе «Определение отношений в моделях» раздела 19.2.
130 Часть II • Создание приложения

9.3. Шаг ГЗ: добавление кнопки


После всего, что уже сделано, настало время добавить к каждому представленному
товару кнопку Добавить в корзину (Add to Cart).
Для этого не нужно создавать новый контроллер или даже новое действие. Если
посмотреть на действия, предоставленные генератором временной платформы,
то среди них можно найти in d e x ( ), show(), new(), e d i t ( ), c r e a t e ( ), u p d a te ( )
и d e s tr o y (). Нашей операции соответствует действие c r e a t e ( ). (Может пока­
заться, что действие new () является аналогичным, но оно используется для по­
лучения формы, применяемой, чтобы запросить ввод для последующего действия
c r e a t e ( ).)
За принятием этого решения следуют все остальные решения. Что именно мы
создаем? Конечно же, не корзину, и даже не товар. Мы создаем товарную позицию
Lineltem. Посмотрев на комментарий, связанный с методом c r e a t e ( ) в файле арр/
controllers/line_items_controller.rb, вы увидите, что в результате этого выбора также
определяется используемый URL-адрес (/linejtems) и H TTP-метод (POST).
Этот выбор даже предлагает правильный элемент управления пользователь­
ского интерфейса. Когда ранее мы добавляли ссылки, мы пользовались методом
l i n k _ t o ( ), но ссылки по умолчанию настроены на использование НТТР-метода
GET. А нам нужно использовать метод POST, потому что на сей раз мы будем
добавлять кнопку а это значит, что мы будем использовать метод b u t to n _ t o ( ).
Мы можем подключить кнопку к товарной позиции, указав URL-адрес, но мы и
в этот раз позволим Rails позаботиться об этом за нас, просто добавив _path к име­
ни контроллера. В данном случае мы воспользуемся lin e _ ite m s _ p a th .
Но при этом возникает проблема: как метод lin e _ ite m s _ p a th узнает, какой
товар добавлять к нашей корзине? Нам нужно передать ему идентификатор товара,
соответствующего кнопке. Делается это довольно просто — нужно лишь к вызову
lin e _ ite m s _ p a th () добавить : p ro d u ct_ id . Можно даже передать сам экземпляр
товара — Rails знает, что при подобных обстоятельствах нужно извлечь из записи
идентификатор.
В конечном итоге одна строка, которую нужно добавить к нашему файлу index,
html.erb, имеет следующий вид:

ra ils3 1 /d e p o t_ f/арр/views/sto re /in d e x.h tm l.e rb


<% i f n o tic e %>
<p id="notice"><%= n o tic e %></p>
<% end %>

<hl>Your Pragm atic Catalog</hl>

<% (Sproducts.each do |product| %>


<div c la s s = " e n t r y " >
<%= im a g e _ta g (p ro d u ct.im a g e _u rl) %>

<h3><%= p r o d u c t . t i t le %></h3>
<%= s a n it iz e ( p r o d u c t .d e s c r ip t io n ) %>
Глава 9 • Задача Г: создание корзины покупателя 131

<div c la s s = "p r ic e _ lin e " >

<span class= "price"> < %= n u m b e r_to _c u rre n c y (p ro d u c t.p ric e ) %></span>


► <%= b u tto n _to 'Add to C a r t ', lin e _ it e m s _ p a t h (p r o d u c t_ id : p ro d uct) %>
</div>
</div>
<% end %>

Здесь возникает еще одни вопрос форматирования. Метод b u t t o n _ t o создает


HTML-тег <form>, и эта форма содержит HTML-тег <div>. Оба они формируют
обычные блоковые элементы, которые должны появляться с новой строки. Мы
хотим, чтобы они появлялись сразу же за ценой, поэтому, чтобы их встроить в ту
же строку нужно добавить немного CSS-волшебства:

ra ils3 1 /d ep o t_ f /арр/a sse ts/ sty le sh e e ts/ sto re .c s s . 4 Н Н Н Н Ш Н Н Н Н Н 1


p, d iv .p r ic e _ lin e {
m a rg in - le ft: 100px;
m argin-top: 0.5em;
margin-bottom: 0.8em;

form, d iv {
d is p la y : in lin e ;
}

Идеально было бы поместить эти строки внутри правила для . e n t r y , которое,


в свою очередь, вложено в правило для . s t o r e .
Теперь наша индексная страница имеет вид, показанный на рис. 9.1. Но, перед
тем как нажать эту кнопку, нам нужно изменить метод c r e a t e ( ) в контроллере
товарных позиций, чтобы в качестве аргументов формы ожидался идентификатор

Pragprog Books Onlirie S

) (ocalhost 3000 ☆ л

P r a g m a t ic B o o k s h e l f
Your Pragmatic Catalog

*** C o ffe e S c rip t


C offeeScript is Ja v a S c rip t done right. It provides all of
J a v a S c rip t's functionality c a p p e d in a cleaner, more
su ccin ct syntax In the first book on this e xcitin g new
language. C o ffeeScrip t guru Trevor Burnham show s you
how to hold onto all the power and flexibility of
J a v a S c rip t while writing clearer, cleaner, and safer code

S36.00 A dd to C o t

Рис. 9.1. Теперь на странице есть кнопка Добавить в корзину (Add to Cart)
132 Часть II • Создание приложения

товара. И здесь мы начинаем понимать всю важность поля идентификатора ( i d )


в наших моделях. Rails идентифицирует объекты модели (и соответствующие стро­
ки базы данных) по их полям id . Если идентификатор передать методу c r e a t e ( ),
добавляемый товар получит уникальную идентификацию.
Почему используется метод c r e a t e ( )? Для ссылки по умолчанию используется
HTTP-метод GET, для кнопки — H TTP-метод POST, a Rails использует эти согла­
шения для определения вызываемого метода. Чтобы увидеть другие соглашения,
взгляните на комментарии внутри файла app/controllers/line_items_controller.rb. Эти
соглашения будут широко использоваться и внутри приложения Depot.
Теперь давайте внесем изменения в L in e lt e m s C o n t r o lle r , чтобы обнаружить
корзину покупателя для текущей сессии (если еще нет ни одной корзины, корзину
нужно добавить), добавить к этой корзине выбранный товар и отобразить содер­
жимое корзины. Нам придется изменить всего лишь несколько строк кода в методе
c r e a t e ( ) файла app/controllers/line_items_controller.rb1:

rails31/depot_f/app/controllers/line_item s_controller.rb
d ef c re a te
► @ cart = c u r r e n t_ c a r t
► product = Product.find(param s[ :p r o d u c t _ id ])
► @ lin e _ite m = @ c a rt. lin e _ it e m s . b u ild (p r o d u c t: prod uct)

respond_to do |form at|

i f @ lin e _ ite m .s a v e
► fo rm a t.h tm l { r e d ir e c t _ t o @ lin e _ it e m .c a r t,
n o tic e : 'L in e item was s u c c e s s f u lly c r e a t e d .' }

fo rm a t.js o n { render js o n : @ lin e _ite m ,


s t a tu s : : c re a te d , lo c a t io n : @ lin e _ite m }
e ls e
fo rm a t.h tm l { render a c tio n : "new" }
fo rm a t.js o n { render js o n : @ lin e _ it e m .e r r o r s ,
s t a tu s : : u n p ro c e s s a b le _ e n tity }
end
end
end

Для обнаружения (или создания) корзины в сессии мы используем метод


c u r r e n t _ c a r t ( ), реализованный в шаге Г1. Затем мы используем объект params
для получения из запроса аргумента : p r o d u c t _ i d . Объект params играет в при­
ложениях Rails довольно важную роль. Он содержит все аргументы, переданные
в запросе браузера. Мы сохраняем результат в локальной переменной, потому что
его не нужно делать доступным представлению.
Затем мы передаем найденный товар в @ c a r t . l i n e _ i t e m s . b u i l d . Благодаря
этому создается новая взаимосвязь товарной позиции между объектом @ c a r t
и товаром. Взаимосвязь можно создать с любого конца, и Rails позаботится об
установке подключений с обеих сторон.

1 Некоторые строки пришлось переносить на следующую строку, чтобы они помести­


лись на странице.
Глава 9 • Задача Г: создание корзины покупателя 133

Получившуюся товарную позицию мы сохраняем в переменной экземпляра по


имени @ line_item .
Остальной код этого метода берет на себя обработку ошибок, более подробное
рассмотрение которой будет в разделе 10.2 «Шаг Д2: обработка ошибок», и обработ­
ку JSON-запросов. Но сейчас нам нужно всего лишь изменить еще одну вещь: когда
создана товарная позиция, нужно перенаправить вас на корзину, вместо того чтобы
вернуть к самой товарной позиции. Поскольку объект товарной позиции знает, как
найти объект корзины, нам нужно лишь добавить . c a n t к вызову метода.
Поскольку мы изменили функцию нашего контроллера, мы знаем, что нужно
будет изменить соответствующий функциональный тест. Мы должны передать
вызову идентификатор товара для создания и изменения того, что мы ожидаем
в качестве цели переадресации. Это будет сделано путем обновления файла test/
functional/lineJtems_controller_test.rb.

ra ils3 1 /d ep o t_ g / te st/fu n ctio n al/lin e_ ite m s_ co n tro lle r_ te st. rb


t e s t "should c re a te lin e _ it e m " do
a s s e r t _ d if f e r e n c e ( ' L in e lte m .c o u n t' ) do
► post : c r e a t e , p ro d u c t_id : p ro d u c ts (: r u b y ) . id
end
► a s s e r t _ r e d ir e c te d _ to c a r t _ p a t h ( a s s ig n s ( : l i n e _ it e m ) . c a r t )
end

До сих пор мы еще ничего не говорили о методе a s s ig n s , поскольку он при­


сутствовал в сгенерированной временной платформе. Этот метод дает нам доступ
к переменной экземпляра, которая была определена (или могла быть определена)
действиями контроллера для использования в представлении.
А теперь мы перезапустим функциональные тесты:
depot> rake t e s t :f u n c t io n a ls

Будучи уверенными, что код работает в соответствии со своим предназначени­


ем, мы пробуем воспользоваться кнопками Добавить в корзину (Add to Cart) в нашем
браузере.
И перед нами предстает следующая картина:

ъЗ й й [ О locathost : 'Ч

P r a g m a t ic B o o k s h e l f

Hom e Line item was successfully created.


Q u e stio n s
New s
C o nta ct
134 Часть II • Создание приложения

В восторг она, конечно, не приводит. Несмотря на то что мы создали для корзи­


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

rails31/depot_f/app/view s/carts/show .htm l.erb


<% i f n o tic e %>
<p id="notice"><%= n o tic e %></p>
<% end %>
<h2>Your Pragm atic Cart</h2>

<% @ c a r t. li n e _ it e m s . each do |item j %>


<li><%= it e m .p r o d u c t . t it le %></li>
<% end %>
</ul>

Итак, когда все это скомпоновано, щелкнем на кнопке Обновить (Refresh) своего
браузера и увидим отображение нашего простого представления:

Ргадргод Books Online


ш ш ш ш щ
т lo c a lh o s t

P r a g m a t ic B o o k s h e l f

Q uestions
fvews
Cornet

Вернитесь по адресу http://localhost:3000/ к странице главного каталога и добавьте


к корзине другой товар. Вы увидите в вашей корзине две исходные записи плюс
наш новый элемент. Похоже, что сессия работает. Настало время продемонстриро­
вать все это заказчику, поэтому мы его позвали и с гордостью продемонстрировали
нашу суровую новую корзину. Но он смутил нас своей реакцией, издав цокающий
звук, предвещающий замеченное им наше явное упущение.
Заказчик объяснил, что в настоящей корзине покупателям не показывают двумя
отдельными строками один и тот же товар. Вместо этого строка товара показыва­
ется всего один раз с указанием количества, которое в данном случае равно двум.
Похоже, наметилась паша следующая задача.
Глава 9 • Задача Г: создание корзины покупателя 135

9.4. Наши достижения


У нас был насыщенный событиями, продуктивный день. К магазину была до­
бавлена корзина покупателя, и в процессе работы мы углубились в некоторые
необходимые свойства Rails:
0 В одном запросе мы создали объект корзины C art и получили возможность
успешно обнаружить эту самую корзину в последующих запросах, используя
объект сессии.
0 Мы добавили закрытый метод в базовый класс для всех наших контроллеров,
сделав его доступным для всех наших контроллеров.
0 Мы создали взаимосвязи между корзинами и товарными позициями и взаи­
мосвязи между товарными позициями и товарами и получили возможность
перемещения с использованием этих взаимосвязей.
0 Мы добавили кнопку для переноса товара в корзину, которая заставляет
создать новую товарную позицию.

Чем заняться на досуге


Попробуйте проделать все это без посторонней помощи:
О Добавьте новую переменную к сессии для записи количества обращений
пользователя к действию index контроллера s to r e . Учтите, что при первом
обращении к данной странице вашего счетчика в сессии еще не будет. Этот
факт можно проверить с помощью следующего кода:
if session[icounten].nil?

Если переменная сессии отсутствует, ее нужно инициализировать. После


этого появится возможность увеличивать ее значение.
О Передайте этот счетчик своему шаблону и отобразите его значение в верхней
части страницы каталога. Подсказка: при формировании отображаемого
сообщения можно воспользоваться вспомогательным методом p lu r a l iz e ,
который переводит слово в множественную форму (определение метода
дано в разделе 21.5).
О Сбросьте счетчик в нуль, когда пользователь что-нибудь добавит в свою
корзину.
О Внесите в шаблон изменения, приводящие к отображению счетчика только
в том случае, если он имеет значение больше пяти.
(П одсказки можно найти по адресу: http://www.pragprog.com/wikis/wiki/
RailsPlayTime.)
Задача Д: усовер­
шенствованная
корзина

О сн о в н ы е тем ы :

> изменение схемы и сущ ествую щ и х данных;


> ди агн ости ка и обработка ош ибок;
> флэш;
> ведение ж урнала.

Примитивная функциональность реализованной нами корзины предполагает


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

10.1. Шаг Д1: создание


усовершенствованной корзины
Привязка счетчика к каждому товару в нашей корзине потребует от нас внести
изменения в таблицу lin e _ ite m s . Ранее мы уже использовали миграции, напри­
мер, для обновления схемы базы данных в разделе 6.1. Несмотря на то что это ис­
пользование было частью создания исходной временной платформы для модели,
базовые подходы остаются прежними.
Глава 10 • Задача Д: усовершенствованная корзина 137

depot> r a i l s g en erate m ig ratio n a d d _ q u a n tity _ to _ lin e _ ite m s q u a n t it y : in te g e r

Исходя из имени миграции, Rails может сообщить, что вы добавляете один или
несколько столбцов к таблице l i n e _ i t e m s , и может взять имена и типы данных для
каждого столбца из последнего аргумента. Rails ищет соответствие двум шаблонам:
add_ X X X _ to_ T A B L E и re m o v e _ X X X _ f rom_TABLE, где значение ХХХ игнорируется.
Значение имеет список имен столбцов и типов данных, появляющийся после имени
миграции.
Rails не может только сообщить, какое приемлемое значение по умолчанию
может быть выбрано для столбца. Во многих случаях подойдет нулевое значение,
но давайте перед применением миграции внесем в нее изменение и сделаем это
значение для существующих корзин равным 1:

rails31/depot_g/db/migrate/20110711000004_add_quantity_to_line_items.rb
c la s s A ddQ uantityToLineltem s < A c tiv e R e c o rd : m ig r a tio n
d ef change
► add_column :lin e _ it e m s , :q u a n t it y , :in t e g e r , d e f a u lt : 1
end
end

После внесения изменения запустим миграцию:


depot> rake d b :m igrate

Теперь в нашей корзине C a r t нужен толковый метод a d d _ p r o d u c t (), который


будет проверять, имеется ли добавляемый товар в нашем списке позиций, и, если
такой товар уже есть, будет повышать его количество, а если его не было, будет
создавать новый объект L in e l t e m :

rails3 1/d ep ot _g/app/models/cart. rb


d e f ad d _p ro d u c t(p ro d u c t_id )
c u rre n t_ ite m = lin e _ ite m s ,fin d _ b y _ p ro d u c t_ id (p ro d u c t_ id )
i f c u rre n t_ ite m
c u r r e n t_ ite m .q u a n t it y += 1
e ls e
c u rre n t_ ite m = lin e _ it e m s . b u ild (p r o d u c t _ id : p ro d u c t_ id )
end
c u rre n t_ ite m
end

В этом коде используется небольшой хитрый трюк Active Record. Как види­
те, в первой строке метода вызывается метод f i n d _ b y _ p r o d u c t _ i d (). Но метод
с таким именем мы не определяли. Тем не менее Active Record замечает вызов
неопределенного метода и опознает, что его имя начинается со строкового зна­
чения fin d_ by (искать по) и заканчивается именем столбца. Затем он в динами­
ческом режиме создает для нас поисковый метод, добавляя его к нашему классу.
Более подробно динамически создаваемые поисковые методы будут рассмотрены
в разделе 19.3.
Чтобы воспользоваться этим методом, нам понадобится также внести изменения
в контроллер товарных позиций:
138 Часть И • Создание приложения

rails31/depot_g/app/controllers/line_item s_controller. rb
d ef c re a te
(Scart = c u r r e n t_ c a r t
product = P ro d u c t.fin d (p a ra m s [:p ro d u c t_ id ])
► @ lin e _ ite m = @ c a rt.a d d _ p ro d u c t(p ro d u c t.id )
respond_to do |form at|
i f @ lin e _ ite m .s a v e
fo rm a t.h tm l { r e d ir e c t _ t o @ lin e _ it e m .c a r t,
n o tic e : 'L in e item was s u c c e s s f u lly c r e a t e d .' }
fo rm a t.js o n { render js o n : @ lin e _ite m ,
s t a tu s : : c re a te d , lo c a t io n : @ lin e _ite m }
e ls e
fo rm a t.h tm l { render a c tio n : "new" }
fo rm a t.js o n { render js o n : @ lin e _ it e m .e r r o r s ,
s t a tu s : : u n p ro c e s s a b le _ e n tity }
end
end
end

И еще одно небольшое изменение в представлении для использования этой новой


информации:

rails31/depot_g/app/views/carts/show. html. erb


<% i f n o tic e %>
<p id="notice"><%= n o tic e %></p>
<% end %>

<h2>Your Pragm atic Cart</h2>


<ul>
<% g c a r t . lin e _ it e m s . each do |item | %>
► <li><%= ite m .q u a n tity %> & tim es; <%= it e m .p r o d u c t . t it le %></li>
<% end %>
</ul>

Когда все расставлено по местам, можно вернуться на страницу магазина и


щелкнуть на кнопке Добавить в корзину (Add to Cart) напротив товара, уже имею­
щегося в корзине. Скорее всего, мы увидим смесь из единичных товаров, пере­
численных отдельно, и один товар, напротив которого в списке будет указано
количество, равное двум. Причина в том, что мы добавили количество, равное
единице, к существующим столбцам, вместо того чтобы свернуть несколько
строк в одну там, где это возможно. Стало быть, теперь нам нужно осуществить
миграцию данных.
Начнем с создания миграции:
depot> r a i l s generate m igration com bine_items_in_cart

На этот раз Rails не в состоянии предположить, что мы пытаемся сделать, поэто­


му мы не можем положиться на сгенерированный метод change(). Вместо этого
нам нужно заменить этот метод отдельными методами и р() и down(). Сначала
создадим метод ир():
Глава 10 • Задача Д: усовершенствованная корзина 139

rails31/depot_g/db/migrate/20110711000005_coinbine_items_in_cart.rb
d ef up
# замена нескольких записей для одного и того же товара в корзине одной записью
C a r t . a l l. e a c h do |c a r t|
# подсчет количества каждого товара в корзине
sums = c a r t . lin e _ it e m s . g ro u p (: p r o d u c t_ id ). sum (: q u a n t ity )

sums.each do |p ro d u c t_id , q u a n tity l


i f q u a n tity > 1
# удаление отдельных записей
c a r t . lin e _ ite m s .w h e re (p ro d u c t_ id : p r o d u c t _ id ) .d e le t e _ a ll

# замена одной записью


c a r t . lin e _ it e m s . c r e a t e (p r o d u c t _ id : p ro d u c t_id , q u a n t ity : q u a n t ity )
end
end
end

Это, несомненно, самый объемный код из всех ранее попадавшихся. Давайте


разберем его по частям:
О Сначала задается перебор содержимого каждой корзины.
О Для каждой корзины мы получаем сумму значений полей количества
( q u a n tity ) для каждой из товарных позиций, связанных с этой корзиной,
сгруппированных по полю идентификатора товара product_id. Получающи­
еся суммы станут списком упорядоченных пар значений полей p ro d u c t_ id
и количества q u a n tity .
О Мы перебираем эти суммы, извлекая из каждой p ro d u c t_ id и q u a n tity .
О В тех случаях, когда количество превышает единицу, мы будем удалять все
отдельные товарные позиции, связанные с этой корзиной и этим товаром,
и заменять их одной товарной позицией с правильным количеством.
Заметьте, как просто и элегантно Rails позволила вам выразить этот алго­
ритм.
Имея данный код, мы применим эту миграцию тем же способом, что и любые
другие миграции:
depot> rake db :m igrate

Мы можем тут же увидеть результаты, взглянув на корзину, которая будет


выглядеть, как показано на рис. 10.1. Хотя у нас есть повод для удовлетворения,
работа еще не закончена.
Важным принципом миграций является обратимость каждого шага, поэтому
мы реализуем так же и метод down(). Этот метод находит товарные позиции со
значением ноля количества больше единицы; добавляет новые товарные позиции
для этой корзины и товара, каждая из которых имеет значение поля количества,
равное единице; и в завершение удаляет исходную товарную позицию. Все это
делается с помощью следующего кода:
140 Часть II • Создание приложения

rails3X/depot_g/db/migrate/20110711000005_combine_items_in_cart.rb
d ef down
# разбиение записей с q u a n tity > l на несколько записей
L in e lte m .w h e re ("q u a n tity > l” ) .each do |lin e _ ite m |
# add in d iv id u a l item s
lin e _ it e m .q u a n t it y .t im e s do
L in e lte m .c re a te c a r t _ id : lin e _ it e m . c a r t _ id ,
p ro d u c t_id : lin e _ it e m .p r o d u c t_ id , q u a n t ity : 1
end

# удаление исходной записи


lin e _ it e m . d estro y
end

PRAliMATH. -B o o k s h e l f

Hom e Your Pragmatic Cart


Ntews • 3 * Program m ing Ruby i.9
C ontact • 1 * CoffeeScript

l'

Рис. 10.1. Корзина с указанием количества

Теперь мы можем просто отменить нашу миграцию с помощью всего лишь


одной команды:
depot> rake d b :ro llb a c k

Мы снова можем тут же посмотреть на результат, взглянув на корзину, которая


показана на рис. 10.2. Применив эту же миграцию еще раз (с помощью команды гаке
d b :m ig rate), мы получим корзину, которая обслуживает счетчик для каждого со­
держащегося в ней товара, и мы получим представление, отображающее этот счетчик.
Обрадованные наличием презентабельного результата, мы позвали заказчика
и показали ему результат нашей утренней работы. Он смог увидеть, как сайт стал
превращаться в единое целое, и был доволен. Но он выразил беспокойство тем, что
узнал из недавно прочитанной в прессе статьи о способах ежедневных атак и взломов
коммерческих сайтов. Он прочитал, что одним из способов атак является выдача
веб-приложениям запросов с неверными аргументами в надежде на вскрытие оши­
бок и изъянов безопасности. Он заметил, что ссылка на корзину выглядит как carts/
ппп, где ппп — наш внутренний идентификатор корзины. В роли злоумышленника
он вручную набрал такой запрос в браузере, указав вместо идентификатора кор­
зины слово wibble. Он был разочарован, когда наше приложение вывело страницу,
Глава 10 • Задача Д: усовершенствованная корзина 141

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

'i# Й [ Q lo c athost

P r a g m a t ic B o o ksh elf

Home
Y o u r P ra g m a tic C art
Q uestions
News • 1 x CoffeeScript
Contact • 1 * Program m ing Ruby 1.9
• 1 * Program m ing Ruby 1.9
• 1 * Program m ing Ruby 1.9

Рис. 10.2. Вид корзины после того, как миграция была отменена

jV ...
Action Controller: Excep

w' ьм ! o tocalhost

A ctiveR eco rd :: Rec<ordN otFound in


CartsControi!er#sn ow
c o u ld n 't f in d c a r t w it h id = w ib b le

И
R a i l s . r o o t : /home/rubys/ 5v n / r a i l s 4 / B o o k / u til/ w o rk - 192 /depot

Application T ra ce j Framework Trace ! Full Trace

a p p / c o n t r o i le r s / c a r t s „ c o n t r o lle r . r b : 16: in show'

Рис. 10.3. Наше приложение раскрывает свое внутреннее устройство

10.2. Шаг Д2: обработка ошибок


Если посмотреть на страницу, показанную на рис. 10.3, станет понятно, что наше
приложение выдало исключение в строке 16 контроллера корзин1, то есть в строке:
@ cart = C art .find (params [ : i d ] )

1 У вас может быть другой номер строки. В наших исходных файлах форматирование
иногда обусловлено размерами книжной страницы.
142 Часть И • Создание приложения

Если корзина не может быть найдена, Active Record выдает исключение


RecordNotFound, которое нам, очевидно, нужно обработать. Возникает вопрос,
как это сделать?
Мы можем просто молча его проигнорировать. Может быть, с точки зрения
безопасности это будет наилучшим выходом из ситуации, поскольку потенци­
альный злоумышленник не получает при этом никакой нужной для себя ин­
формации. Но это также означает, что при наличии в нашем коде ошибки, из-за
которой генерируются неверные идентификаторы корзин, для внешнего мира
наше приложение перестанет на что-либо реагировать, и о наличии ошибки так
никто и не узнает.
Но мы поступим по-другому, и при выдаче исключения предпримем два дей­
ствия. Во-первых, мы зарегистрируем этот факт во внутреннем журнале регистра­
ции, использовав имеющиеся в Rails соответствующие возможности1. Во-вторых,
мы повторно выведем страницу каталога с коротким сообщением для пользовате­
лей (что-нибудь вроде «Несуществующая корзина»), чтобы они смогли продолжать
пользоваться нашим сайтом.
В Rails имеется удобный способ обработки ошибок и уведомлений об их воз­
никновении. Он заключается в определении структуры под названием флэш. Это
область памяти (что-то близкое к хэшу), где вы можете хранить материал при
обработке запроса. Содержимое флэш-области перед операцией автоматического
удаления доступно следующему запросу в сессии. Обычно флэш используется
для сбора сообщений об ошибках. К примеру, когда наш метод show() обнаружит,
что ему был передан негодный идентификатор корзины, он может сохранить со­
общение об ошибке во флэш-области и перенаправить ход выполнения программы
на действие index, чтобы снова показать каталог. Представление, следующее за
выполнением действия in dex, может извлечь сообщение об ошибке и показать
его в верхней части страницы каталога. Информация флэш-памяти доступна из
представления при использовании метода flash.
А почему мы не можем хранить информацию об ошибках в любой существую­
щей переменной экземпляра? Не забывайте, что после того, как сообщение о не­
правильном идентификаторе отослано приложением браузеру, тот посылает в ответ
приложению новый запрос. За то время, пока он будет идти, приложение не будет
стоять на месте, и все переменные экземпляра, существовавшие во время предыду­
щих запросов, канут в Лету. А флэш-данные хранятся в сессии для того, чтобы быть
доступными между запросами.
Вооружившись познаниями о флэш-данных, мы теперь можем внести измене­
ния в наш метод show () и научить его перехватывать негодные идентификаторы
корзин и сообщать нам о характере проблемы:

ra ils3 1 / d e p o t_ h / a p p / co n tro lle rs/ ca rts_ co n tro lle r.rb


# GET / c a rts / 1
# GET / c a r ts / 1 .js o n
d e f show
► begin

1 http://guides.ruby0nrails.0rg/debugging_rails_applicati0ns.htm l#the-l0gger
Глава 10 • Задача Д: усовершенствованная корзина 143

(Scart = C a rt .fin d (p aram s[: i d ] )


► rescue A c tiv e R e c o rd : : RecordNotFound
► lo g g e r .e r r o r "Попытка доступа к несуществующей корзине # {p a ra m s[: i d ] } "
► r e d ir e c t _ t o s t o r e _ u r l, n o tic e : 'Несуществующая корзина'
► e ls e
respond_to do |form at|
fo rm a t.h tm l # show .htm l. erb
fo rm a t.js o n { render js o n : (Scart }
end
► end
end

Оператор re s c u e перехватывает исключение, выданное C a rt.fin d (). А в об­


работчике исключения мы делаем следующее:
О Используем Rails-регистратор для записи ошибки. Свойство lo g g e r есть
у каждого контроллера. В данном случае мы используем его для записи со­
общения в регистрационный уровень e rr o r .
О Переадресовываем запрос на отображение каталога, используя метод
r e d i r e c t _ t o ( ). Аргумент : n o tic e определяет сообщение, которое будет
сохранено во флэш-области в качестве уведомления. А зачем нам переадре­
сация, если можно просто отобразить каталог? Если мы применим переа­
дресацию, в браузере будет выставлен URL-адрес магазина, а не http://.../
cart/wibble. Таким образом, мы выставляем напоказ меньшую часть нашего
приложения, а также оберегаем пользователя от повторного инициирования
ошибки в случае перезагрузки страницы.
Имея такой код, мы можем перезапустить проблемный запрос нашего заказчика.
На этот раз при вводе следующего URL-адреса:
h t t p :/ / lo c a lh o s t : 3 00 0/carts/w ibb le

мы уже не видим в браузере целую группу сообщений об ошибках. Вместо этого


отображается страница каталога. Если посмотреть на последние записи регистра­
ционного журнала (development.log в каталоге log), мы увидим наше сообщение:
S ta r t e d GET "/ c a r ts / w ib b le " f o r 1 2 7 .0.0.1 a t 2011-05-27 12:16:28 -0400
Pro ce s sin g by C a rtsC o n tro lle r# s h o w as HTML
Param eters: { " i d ” = > "w ibble"}
Л[ [1 т Л[ [ 35mCart Load ( 0 . lm s )A[[0m SELECT " c a r t s " . * FROM " c a r t s " WHERE
" c a r t s " . " i d " = ? LIM IT 1 [ [ " i d " , " w ib b le " ]]
► Попытка доступа к несуществующей корзине
R e d ire c te d to h ttp :/ / lo c a lh o s t:3 0 0 0 /
Completed 302 Found in 3ms

А более удобный для пользовательского восприятия результат показан на


рис. 10.4.
На Unix-машинах для просмотра этого файла мы, скорее всего, воспользу­
емся командой t a i l или l e s s . А в Windows можно воспользоваться привыч­
ным текстовым редактором. Неплохо было бы держать открытым окно, в кото­
ром отображаются добавляемые в файл новые строки. Для этого в Unix нужно
144 Часть И • Создание приложения

Pragprog Books Online V •Ш . !?

, ;■ lo c a lh o s t

P r a g m a t ic B o o ksh elf

Your Pragmatic Catalog

Coffe e Script
CoffeeScrip t is Ja va S crip t clone right. It provides
all of Ja v a S c rip t's functionality wrapped in a
cleaner, more su c c in c t syntax. In the first book
on this exciting new language. C o ffeeScrip t gum
Trevor Burnham show s yo u how to hold onto all
the power and flexibility of Ja va S crip t while
writing clearer, cleaner and safer code

Рис. 10.4. Более удобное для пользователя сообщение об ошибке

воспользоваться командой t a i l - f . Команду t a i l для Windows можно загру­


зить с веб-сайта http://gnuwin32.sourceforge.net/packages/coreutils.htm или получить
средство с графическим интерфейсом с веб-сайта http://tailforwin32.sourceforge.net/.
И наконец, некоторые пользователи OS X для отслеживания регистрационных
файлов используют Console.арр. Для этого нужно в командной строке набрать ко­
манду open им я. log.
Чувствуя близость завершения очередного шага, мы позвали заказчика и по­
казали ему, что теперь приложение осуществляет вполне приемлемую обработку
ошибки. Это его обрадовало, и он продолжил испытание нашего приложения на
прочность. При этом он обнаружил в нашем новом отображении корзины не­
большой изъян — отсутствие способа опустошения корзины. Эта небольшая до­
работка и станет нашим следующим шагом, который мы сделаем прежде, чем уйти
с работы.

10.3. Шаг ДЗ: завершение


разработки корзины
Теперь мы знаем, что для реализации функции «пустая корзина» нужно добавить
ссылку на корзину и изменить метод d e s tro y () в контроллере корзины, чтобы он
очищал сессию. Начнем с шаблона и, чтобы поместить кнопку на страницу, опять
воспользуемся методом b u tto n _ to ( ):
Глава 10 • Задача Д: усовершенствованная корзина 145

rails31/depot_h/app/views/carts/show.htm l.erb
<% i f n o tic e %>
<p id="notice"><%= n o tic e %></p>
<% end %>

<h2>Your Pragm atic Cart</h2>


<ul>
<% (S c a rt. lin e _ it e m s . each do | item | %>
<li><%= ite m .q u a n tity %> &tim es; <%= it e m .p r o d u c t . t it le %></li>
<% end %>
</ul>

►<%= b u tto n _to 'Empty c a r t ', @ cart, method: :d e le te ,


► confirm: 'A re you s u r e ? ' %>

Чтобы гарантировать, что перед перенаправлением на главную страницу с уве­


домительным сообщением пользователь удаляет содержимое своей собственной
корзины (что немаловажно!), в контроллере будет изменен метод d e s tro y ():

ra ils3 1 / d e p o t_ h / a p p / co n tro lle rs/ ca rts_ co n tro lle r.rb


d ef d e stro y
► @ cart = c u r r e n t_ c a r t
@ c a rt. d e stro y
► s e s s io n f : c a r t _ id ] = n i l
respond_to do |form at|
► fo rm a t.h tm l { r e d ir e c t _ t o s t o r e _ u r l,
► n o tic e : 'Теперь ваша корзина п у с т а !' }
fo rm a t.js o n { head :ok }
end
end

Мы также обновляем соответствующий тест в файле test/functiorial/carts_controller_


test.rb.
ra ils 3 1 / d e p o t_ i/ te s t/ fu n c tio n a l/ c a rts _ c o n tro lle r_ te s t. rb
t e s t "should d e stro y c a r t " do
a s s e r t _ d if f e r e n c e ( ' C a r t . c o u n t' , -1) do
► s e s s io n [ : c a r t _ id ] = (S c a r t .id
d e le te :d e s tro y , id : @ cart.to_param
end
► a s s e r t _ r e d ir e c te d _ to sto re _p ath
end

Теперь при просмотре корзины по щелчку на кнопке Опустошить корзину мы


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

Y o u r cart is currently em pty


146 Часть II • Создание приложения

Нам также нужно удалить флэш-сообщение, автоматически генерируемое при


добавлении товарной позиции:

r a i l s 3 1 / d e p o t _ i / a p p / c o n t r o l l e r s / l i n e _ i t e m s _ c o n t r o l l e r . rb
d ef c re a te
@ cart = c u rre n t_ c a r t
product = Produ ct .fin d (p aram s[: p r o d u c t_ id ])
@ lin e _ ite m = (fflcart. a d d _p ro d u ct(p ro d u ct. id )
respond_to do |form at|
i f @ lin e _ ite m .s a v e
► fo rm a t.h tm l { r e d ir e c t _ t o @ lin e _ it e m .c a r t }
fo rm a t.js o n { render js o n : (S lin e it e m ,
s t a tu s : : c re a te d , lo c a t io n : @ lin e _ite m }
e ls e
fo rm a t.h tm l { render a c tio n : "new" }
fo rm a t.js o n { render js o n : @ lin e _ it e m .e r r o r s ,
s t a tu s : : u n p ro c e s s a b le _ e n tity }
end
end
end

Д ЭВИД ГОВОРИТ: БИТВА МАРШ РУТОВ: PRODUCT_PATH


ПРОТИВ P R O D U C T _ U R L ---------------------------------------------------------------------------
Поначалу кажется, что будет довольно слож но узнать, когда при необходимости создать
ссы лку или перенаправление по заданному марш руту нужно использовать метод product^
path, а когда метод product_url. Но на самом деле все довольно просто.
При использовании метода p r o d u c tjjrl вы получите полную начинку с протоколом и до­
менным именем, наподобие http://exam ple.c0m /pr0d u c t s / l. Его следует использовать, если
осущ ествляется перенаправление redirect._to, поскольку спецификация HTTP при осущ ест­
влении перенаправлений с кодом 302 и им подобных требует указывать URL-адрес полно­
стью. Полный URL-адрес нужен также при перенаправлении с одного домена на другой,
например, product_ur!(dom ain: "exam ple2.com ", product: product).
Во всех остальных случаях можно с успехом использовать product_path. Этот метод будет
генерировать только часть пути /products/1, а для ссылок или указания форм вроде link_to
"My lovely product", p ro d u c tjia th (p ro d u c t) больше ничего и не нужно.
Путаница возникает из-за того, что снисходительность браузеров зачастую делает эти два
метода взаимозаменяемыми. Перенаправление re d ir e c t jn можно производить с product_
path, и такой вариант, скорее всего, сработает, но с точки зрения спецификации он будет
считаться неправильным. Точно так ж е можно при ссылке link_to использовать product_url,
но тогда ваш HTML будет засорен ненужными символами, что также нельзя признать удач­
ным вариантом.

И наконец, мы наведем порядок в отображении корзины. Давайте вместо ис­


пользования для каждой записи элементов <1 i > воспользуемся таблицей. Стили
опять же будут задаваться с помощью CSS:
r a ils 3 1 / d e p o t _ i/ a p p / v ie w s / c a r t s / s h o w . h t m l. e r b
<% i f n o tic e %>
<р id="notice"><%= n o tic e %></p>
Глава 10 • Задача Д: усовершенствованная корзина 147

<% end %>

<div c la s s = " c a r t _ t it le " > Y o u r Cart</div>


<table>
<% @ c a rt. lin e _ ite m s .e a c h do |item j %>
<tr>
<td><%= ite m .q u a n tity %>&times;</td>
<td><%= it e m .p r o d u c t . t it le %></td>
<td class= "item _price"> < %= n u m b e r_ to _ c u rre n c y (ite m .to ta l_ p ric e ) %></td>
</tr>
<% end %>
<tr c la s s = " t o t a l_ lin e " >
<td colspan="2">Total</td>
<td c l a s s = " t o t a l _ c e l l " ><%= n u m b e r_ to _ c u rre n c y ((a lc a rt.to ta l_ p ric e ) %></td>
</tr>
</table>

<%= b u tto n _to 'Empty c a r t ' , @ c a rt, method: : d e le te ,


confirm: 'A re you s u r e ? ' %>

Чтобы все это заработало, нам нужно к обеим моделям, L in e lte m и C a rt,
добавить метод, возвращающий общую стоимость, соответственно, для каждой
товарной позиции и для всей корзины. Сначала займемся товарной позицией, где
понадобится только простое умножение:

rails31/depot_i/app/m odels/line_item .rb


d e f t o t a l _ p r ic e
p ro d u c t. p r ic e * q u a n tity

М етод в модели C a r t мы реализуем с использованием R ails-метода


A rray : :sum (), который суммирует цены каждой записи, имеющейся в коллек­
ции:

rails31/depot_i/app/m odels/cart. rb
def t o t a l _ p r ic e
lin e _ it e m s . t o _ a . sum { |item | it e m .t o t a l_ p r ic e }

Затем нам нужно будет добавить небольшой фрагмент к нашей таблице стилей
carts.css.scss:
ra ils3 1 / d e p o t_ i/ a p p / a sse ts/ s ty le sh e e ts/ ca rts.cs s.sc ss
// P la c e a l l th e s t y le s r e la te d to th e C a rts c o n t r o lle r h ere.
// They w i l l a u to m a tic a lly be in clu d ed in a p p lic a t io n . c s s .
// You can use Sass (SC SS) h ere: h ttp :// sa ss- lan g .co m /

.c a r t s {
.c a r t_ title {
fo n t : 120% bold;
}
148 Часть II • Создание приложения

. ite m _ p ric e , . t o t a l _ l i n e {
t e x t - a lig n : r ig h t ;
}
.t o ta l_ lin e .t o t a l_ c e ll {
fo n t- w e ig h t: bold;
b ord er-top: lp x s o lid #595;
}

Улучшенное изображение корзины показано на рис. 10.5.

Ргagprog Books Online St

'J ' ЙО' ( О localhost 3000/. "SI л

P r a g m a t ic B o o ksh elf

Your Can
Home
2* CoffeeScrip t $72.00
Q uestions
Nev/s 1* Program m ing Ruby 1 9 $49.95

Contact Total S121.95

Em pty cart

Рис. 10.5. Отображение корзины с общей суммой (Total)

10.4. Наши достижения


Теперь наша корзина покупателя может порадовать заказчика. При решении
нашей задачи мы прошли следующие этапы:
0 добавление к существующей таблице столбца, имеющего значение по умол­
чанию;
0 миграция существующих данных в новый табличный формат;
0 предоставление флэш-сообщения об обнаруженной ошибке;
0 использование регистратора для записи событий в журнал;
0 удаление записи;
0 настройка способа отображения таблицы с использованием CSS.
Но когда мы стали подумывать о том, что дело в шляпе, наш заказчик просма­
тривал экземпляр «Information Technology and Golf Weekly». Видимо, там была
статья о новом стиле интерфейса, используемого на стороне браузера, который
позволяет обновлять содержимое на лету. «AJAX», — произнес он величественно.
Ну что ж, завтра на него и взглянем.
Глава 10 * Задача Д: усовершенствованная корзина 149

Чем заняться на досуге


Попробуйте проделать все это без посторонней помощи:
О Создайте миграцию, копирующую цену товара в товарную позицию, и изме­
ните метод add_product() в модели C art для получения цены при создании
новой товарной позиции.
О Добавьте блочные тесты, которые добавляют уникальные товары и дублиру­
ют товары. Учтите, что вам придется изменить стенд для ссылки на товары
и корзины по имени, например p r o d u c t: ruby.
О Организуйте проверку товаров и товарных позиций в тех местах, где были
бы уместны полезные для пользователей сообщения об ошибках.
О Добавьте возможность удаления из корзины отдельных товарных позиций.
Для этого могут потребоваться кнопки в каждой строке, и эти кнопки долж­
ны будут ссылаться на действие d e s tro y () в L in e lte m s C o n tro lle r.
(П одсказки можно найти по адресу: http://www.pragprog.com/wikis/wiki/
RailsPlayTime.)
Задача Е;
добавление AJAX

Основные темы:
> и спользование п арциальны х ш аблонов;
> п ерем ещ ение корзины в макет страницы;
> д и нам и ческое обновлен ие стран иц с п ом ощ ью АЗАХ и JavaScript;
> вы деление изм енений с п ом ощ ью jQ u ery UI;
> сокры тие и отображ ен и е D O M -элементов;
> тестирован и е AJAX-обновлений.

Наш заказчик захотел добавить в код интернет-магазина поддержку AJAX. А что


такое AJAX?
В прежние времена (примерно до 200.5 года) считалось, что браузеры не в со­
стоянии обрабатывать данные самостоятельно. При создании приложений, ис­
пользующих браузер, последнему посылались данные, и больше в течение текущей
сессии о нем даже не вспоминали. Временами пользователю нужно было заполнить
поля формы или щелкнуть на гиперссылке, пробуждая приложение входящим за­
просом. В ответ оно посылало пользователю готовую страницу, и весь рутинный
процесс возвращался в прежнее русло. Именно так до сих пор вело себя и наше
приложение Depot.
Но, оказывается, браузеры не столь пассивны (а вы знали об этом?). Они
могут работать с программным кодом. Почти все браузеры в состоянии работать
с JavaScript. Оказалось также, что JavaScript на браузере может в фоновом режиме
взаимодействовать с приложением на сервере, обновляя в результате этого про­
цесса информацию, предоставляемую пользователю. Джесси Джеймс Гаррет (Jesse
James Garrett) назвал такой метод взаимодействия AJAX (что означало когда-то
Глава 11 • Задача Е: добавление AJAX 151

Asynchronous JavaScript and XML, а теперь означает технологию, позволяющую


сократить информационные потребности браузера).
Итак, проведем «аяксификацию» нашей корзины покупателя. Вместо отдельной
страницы для обслуживания корзины поместим отображение информации, при­
надлежащей текущей корзине, на боковую панель каталога. Затем мы применим
волшебство AJAX-технологии для обновления корзины на боковой панели без
повторного отображения всей страницы.
При работе с AJAX для начала лучше создать версию, не использующую эту
технологию, а затем постепенно ввести в нее функции AJAX. Мы так и сделаем. Для
начала переместим корзину с ее собственной страницы на боковую панель.

11.1. Шаг Е1: перемещение корзины


На данный момент наша корзина отображается действием show в C a rtC o n tro lle r
и соответствующим шаблоном .html.erb. Мы же хотим переместить это отображение
в боковую панель, то есть лишить корзину своей собственной страницы. Вместо
этого мы отобразим ее в макете, который выводит весь каталог. Проще всего это
сделать с помощью парциальных шаблонов.

Парциальные шаблоны
Языки программирования предоставляют возможность определения методов. Ме­
тод представляет собой поименованный фрагмент кода: стоит только вызвать метод
по имени, как тут же будет запущен соответствующий фрагмент кода. И, конечно
же, методам можно передавать аргументы, позволяющие создавать один фрагмент
кода, который может быть использован во многих различных обстоятельствах.
Парциальные шаблоны (или, для краткости, парциалы) Rails можно считать не­
кой разновидностью методов для представлений. Парциал —это просто фрагмент
представления, находящийся в своем собственном отдельном файле. Парциал
можно вызвать (отобразить) из другого шаблона или из контроллера, и он ото­
бразит самого себя и вернет результаты этого отображения. И, как и при работе
с методами, парциалу можно передать аргументы, поэтому один и тот же парциал
может отображать разные результаты.
В этом шаге парциалы будут применены дважды. Для начала взглянем на ото­
бражение самой корзины:
rails31/depot_i/app/view s/carts/show .htm l. erb
<% i f n o tic e %>
<p id="notice"><%= n o tic e %></p>
<% end %>

<div c la s s = " c a r t _ t it le " > Y o u r Cart</div>


<table>
<% @ c a rt. lin e __ite m s. each do |item | %>
<tr>
152 Часть II • Создание приложения

<td><%= ite m .q u a n tity %>&times ; </td>


<td><%= it e m .p r o d u c t . t it le %></td>
<td class= "item _price"> < %= n u m b e r_ to _ c u rre n c y (ite m .to ta l_ p ric e ) %></td>
</tr>
<% end %>
<tr c l a s s = " t o t a l _ l i n e " >
<td colspan="2">Total</td>
<td c la s s = "to ta l_ c e ll"> < % = n u m b e r_ to _ c u rre n c y (@ c a rt.to ta l_ p r ic e ) %></td>
</tr>
</table>

<%= b u tto n _to 'Empty c a r t ' , (Scart, method: :d e le te ,


confirm: 'A re you s u r e ? ' %>

Здесь создается список строк таблицы, по одной для каждой записи в корзи­
не. Когда сталкиваешься с подобного рода повторениями, возникает вопрос, а не
многовато ли логики в шаблоне? Оказывается, абстрагироваться от цикла можно
с помощью парпи&тов (вскоре станет ясно, что тем самым мы подготовимся и к бу­
дущему применению магии AJAX).
Для этого воспользуемся возможностью передачи коллекции методу, отобра­
жающему парциальные шаблоны, и этот метод автоматически вызовет парциал по
одному разу для каждого элемента коллекции. Давайте перепишем отображение
корзины, чтобы воспользоваться этой возможностью:

rails31/depot_j/app/view s/carts/show .htm l.erb


<% i f n o tic e %>
<p id = " n o t ic e " ><%= n o tic e %></p>
<% end %>

<div c la s s = " c a r t _ t it le " > Y o u r Cart</div>


<table>
► <%= re n d e r (@ c a r t. lin e _ it e m s ) %>

<tr c la s s = " t o t a l_ lin e " >


<td colspan="2">Total</td>
<td c la s s = "to ta l_ c e ll"> < % = n u m b e r_ to _ c u rre n c y ((S )c a rt.to ta l_ p ric e ) %></td>
</tr>

</table>

<%= b u tto n _to 'Empty c a r t ' , @ cart, method: d e le te ,


confirm: 'A re you s u r e ? ' %>

Ну вот, теперь все стало намного проще. Метод r e n d e r ( ) будет осуществлять


перебор элементов переданной ему коллекции. Сам по себе парциальный шаблон —
это просто еще один файл шаблона (который по умолчанию находится в том же
самом каталоге, что и отображаемый объект, и использует в качестве имени имя
таблицы). Но, чтобы парциалы отличались от обычных шаблонов, Rails при поис­
ке файла автоматически ставит перед именем парциала символ подчеркивания.
Значит, наш парциал нужно назвать _line_item.html.erb и поместить его в каталог
app/views/line_items.
Глава 11 • Задача Е: добавление AJAX 153

rails31/depot_j/app/view s/line_item s/_line_item .htm l.erb


<tr>
<td><%= lin e _ it e m .q u a n t it y %>&times ; </td>
<td><%= li n e _ it e m . p r o d u c t . t it le %></td>
<td class= "item _price"> < %= n u m b e r _ to _ c u r r e n c y (lin e _ ite m .t o ta l_ p r ic e ) %></td>
</tr>

Здесь используется одна интересная особенность. Внутри парциального шабло­


на мы ссылаемся на текущий объект, используя для этого имя переменной, совпа­
дающее с именем шаблона. В данном случае парциал называется lin e_ item , следо­
вательно, внутри парциала ожидается наличие переменной но имени lin e _ ite m .
Итак, мы навели порядок с отображением корзины, но она еще не перемещена
на боковую панель. Для ее перемещения обратимся еще раз к нашему макету. Имея
парциальный шаблон, способный отобразить корзину, мы просто можем вставить
в боковую панель следующий вызов:
r e n d e r ("c a r t")

Но знает ли сейчас парциал о том, где найти объект c a r t? Он мог бы выстро­


ить предположение. В макете у нас есть доступ к переменной экземпляра (fficart,
которая была определена в контроллере. Оказывается, она также доступна внутри
парциала, вызываемого из макета. Это чем-то напоминает вызов метода и передачу
ему какого-то значения глобальной переменной. Все это, конечно, работает, но вы­
глядит неважно, увеличивая к тому же взаимозависимость компонентов программы
(делая ее менее устойчивой и трудной в поддержке).
Получив парциал для товарной позиции, давайте сделаем то же самое и для
корзины. Создадим сначала шаблон _cart.html.erb. Его основу составит наш шаблон
carts/show.html.erb, но с использованием c a r t вместо (Scart и без уведомления. (За­
метьте, что вызов из парциала других парциалов — в порядке вещей.)

ra ils31/depot _ j/app/view s/carts/_cart. htm l. erb


<div c la s s = " c a r t _ t it le " > Y o u r Cart</div>
<table>
► <%= r e n d e r (c a r t .lin e _ it e m s ) %>

<tr c la s s = " t o t a l_ lin e " >


<td colspan="2">Total</td>
► <td c la s s = "to ta l_ c e ll"> < % = n u m b e r _ to _ c u r r e n c y (c a r t.to ta l_ p r ic e ) %></td>
</tr>

</table>

►<%= b u tto n _to 'Empty c a r t ' , c a r t , method: : d e le te ,


confirm: 'A re you s u r e ? ' %>

Мантра Rails гласит: не допускайте повторений — «Don’t Repeat Yourselves»


(DRY), и мы всего лишь следовали этой установке. Теперь эти два файла согласо­
ваны, и может показаться, что проблем с ними больше нет. Но проблемы вызывает
наличие одного набора логики для вызовов AJAX и другого набора логики для
обработки тех случаев, когда JavaScript отключен.
154 Часть II • Создание приложения

Давайте попробуем всего этого избежать и заменим исходный шаблон кодом,


вызывающим отображение парциала:

rails31/depot_k/app/view s/carts/show .htm l.erb


<% i f n o tic e %>
<p id="notice"><%= n o tic e %x/p>
<% end %>

►<%= render (Scart %>

Теперь изменим макет приложения, включив этот новый парциал в боковую


панель:

ra ils31/depot_k/app/view s/layouts/application.h tm l.erb


<!DOCTYPE html>
<html>
<head>
< title > P rag p ro g Books O n line S t o re < / title >
<%= s t y le s h e e t _ lin k _ t a g " a p p lic a t io n ” %>
<%= ja v a s c r ip t _ in c lu d e _ t a g " a p p lic a t io n ” %>
<%= c srf_ m e ta _ta g %>
</head>
<body class="<%= c o n t r o l l e r . co n tro lle r_n a m e %>">
<div id="banner">
<%= im a g e _ ta g ("lo g o .p n g ") %>
<%= @ p a g e _ title || "P rag m a tic B o o k s h e lf" %>
</div>
<div id="columns">
<div id = "sid e">
► <div id = "c a rt">
► <%= render (Scart %>
► </div>

<ul>
< l i x a h r e f= "h tt p : / /www.. . . " >Home</ax/li>
< l i x a h re f= "h t t p : //www. . . ,/ fa q "> Q u e s tio n s < / a x / li>
< l i x a h re f= "http://w w w . .. ,/news"> N ew s< /ax/li>
< l i x a h re f= "h t t p : //www. .. ./ c o n ta c t"> C o n ta c t< / a x / li>
</ul>
</div>
<div id="main">
<%= y i e ld %>
</div>
</div>
</body>
</html>

Далее нужно будет внести небольшие изменения в контроллер магазина. Макет


вызывается при обращении к действию index контроллера магазина, а пока пере­
менной @cart в этом действии присваивается неверное значение. Но это нетрудно
исправить:
Глава 11 * Задача Е: добавление AJAX 155

ra ils3 1 / d e p o t_ k / a p p / co n tro lle rs/ sto re _ co n tro lle r. rb


def index
^products = P r o d u c t , o r d e r ( : t i t l e )
► (Scart = c u r r e n t_ c a r t
end

И наконец, мы изменим инструкции стиля, которые сейчас применяются


только к выводу, производимому C a rtC o n tro lle r, чтобы они применялись также
к таблице, когда она появляется на боковой панели. И здесь SCSS позволяет нам
внести изменения только в одном месте, поскольку эта система позаботится о всех
вложенных определениях.
ra ils3 1 /d ep ot_ k/a p p /a ssets/stylesh eets/ca rts. c s s . scss
// P la c e a l l th e s t y le s re la te d to th e C a rts c o n t r o lle r h ere.
// They w i l l a u to m a tic a lly be in clud ed in a p p lic a t io n . c s s .
// You can use Sass (SC SS ) h ere: h ttp ://sa ss~ la n g .co m /
► .c a r t s , # sid e # c a rt {
.c a r t_ title {
fo n t : 120% bold;
}
,ite m _ p r ic e , , t o t a l _ l i n e {
t e x t - a lig n : r ig h t ;
}
,t o t a l_ lin e .t o t a l_ c e ll {
fo n t- w e ig h t: bold;
bord er-top: Ip x s o lid #595;
}
}
Несмотря на то, что данные для корзины имеют общий характер и не зависят
от того, куда именно помещается вывод, требования, что представление должно
быть таким же независимым от того, куда помещается содержимое, не существует.
Следует заметить, что черные надписи на зеленом фоне слишком плохо читаются,
поэтому давайте предоставим дополнительные правила для этой таблицы при ее
появлении на боковой панели:
ra ils3 1 /d e p o t_ k /a p p /a sse ts/ sty le sh e e ts/ a p p lica tio n .css.scss
# sid e {
flo at: l e f t ;
padding: lem 2em;
w id th : 13em;
background: #141;

► form, d iv {
► d is p la y : in lin e ;
► }

► in p u t {
► fo n t - s iz e : sm a ll;
► }
156 Часть II • Создание приложения

# c a rt {
fo n t - s iz e : s m a lle r;
c o lo r : w h ite ;

t a b le {
b ord er-top: lp x dotted #595;
border-bottom : lp x dotted #595;
margin-bottom: 10px;
}
}
ul {
padding: 0;
И {
lis t - s t y le : none;
a {
c o lo r : #bfb;
fo n t - s iz e : s m a ll;
}
}
}

Если отобразить каталог после добавления товара в корзину, появится картин­


ка, похожая на рис. 11.1. Остается только дождаться наград в номинации Webby
Award.
• .—

ЙййГ (_ lo c a lh o s t

P ra g m л т к : B o o k sh elf
Your Cart Your Pragmatic Catalog
2* CoffeeScript $72.00
^ Programming
$99.90 CoffeeScript
Ruby 1.9
CoffeeScript is Ja va S crip t done right. It provides all of Ja va S crip t’s
Total 5 1 7 1 .9 0
functionality wrapped in a cleaner, more succinct syntax. In the first
book an this exciting new language CoffeeScript guru Trevor
Burnham show s you how to hold onto all the power and flexibility of
JavaScrip t while writing clearer, cleaner, and safer code.
Home
Questions S36.00 Add to Cart
News

Рис. 11.1. Корзина на боковой панели

Смена направления
Теперь, когда корзина отображается на боковой панели, мы можем изменить
характер работы кнопки Добавить в корзину (Add to Cart). Вместо отображения
Глава 11 • Задача Е: добавление AJAX 157

отдельной страницы корзины ей нужно будет всего лишь обновить главную страни­
цу каталога. Изменить ее функции несложно: в конце действия c r e a te мы просто
перенаправим браузер обратно на каталог:

rails31/depot_k/app/controllers/line_item s_controller. rb
d ef c re a te
g c a r t = c u r r e n t_ c a r t
product = P ro d u c t.fin d (p a ra m s [:p ro d u c t_ id ])
@ lin e _ite m = @ c a rt.a d d _ p ro d u c t(p ro d u c t.id )
respond_to do |form at|
i f @ lin e _ ite m .s a v e
► fo rm a t.h tm l { r e d ir e c t _ t o s t o r e _ u r l }
fo rm a t.js o n { render jso n : @ lin e _ite m ,
s t a tu s : : c re a te d , lo c a t io n : @ lin e _ite m }
e ls e
fo rm a t.h tm l { render a c tio n : "new" }
fo rm a t.js o n { render jso n : @ lin e _ it e m .e r r o r s ,
s t a tu s : : u n p ro c e s s a b le _ e n tity }
end
end
end

Итак, теперь у нас есть магазин с корзиной на боковой панели. Когда мы щел­
кнем на кнопке добавления товара к содержимому корзины, страница перезагру-
зится с отображением обновленной корзины. Тем не менее, если каталог слишком
объемный, эта перезагрузка займет довольно много времени. При этом будут за­
действованы ресурсы интернет-канала и сервера. Улучшить создавшуюся ситуацию
позволит технология AJAX.

11.2. Шаг Е2: создание корзины на основе


AJAX-технологии
AJAX позволяет создавать код, выполняемый на стороне браузера и взаимодей­
ствующий с приложением, находящимся на сервере. В нашем случае мы хотели бы
заставить кнопку Добавить в корзину (Add to Cart) вызывать серверное действие
в контроллере L in e lte m s в фоновом режиме. Затем сервер сможет отправить
обратно только лишь HTM L-код, касающийся корзины, а мы сможем заменить
корзину, присутствующую на боковой панели, ее серверным обновлением.
Сейчас было бы вполне естественно сделать это, написав код JavaScript, который
запускается в браузере, и написав код на стороне сервера, который взаимодействует
с этим кодом JavaScript (возможно, с помощью такой технологии, как JavaScript
Object Notation (JSON)).
Но нам повезло, потому что в Rails все это делается вне поля нашего зрения.
Все, что нам нужно, мы можем сделать с помощью Ruby (при широкой поддержке
ряда вспомогательных методов Rails).
Вся хитрость добавления AJAX к приложению состоит в том, чтобы делать
это мелкими шагами. Начнем с самого основного шага и изменим страницу
158 Часть И • Создание приложения

каталога, чтобы она отправляла нашему серверному приложению AJAX-запрос,


заставив приложение отвечать HTM L-фрагментом, содержащим обновленную
корзину.
На странице каталога для ссылки на действие c r e a t e мы используем метод
b u tto n _ to ( ). Нам нужно внести изменения, чтобы отправить вместо этого AJAX-
запрос. Для этого мы просто добавим к вызову фрагмент re m o te : tru e .

rails31/depot_l/app/view s/store/index. htm l. erb


<% i f n o tic e %>
<p id = "n o tic e "x % = n o tic e %></p>
<% end %>

<hl>Your Pragm atic Catalog</hl>

<% ^ p ro d u c ts. each do |product| %>


<div c la s s= "e n try ">
<%= im a g e _ta g (p ro d u ct.im a g e _u rl) %>
<h3><%= p r o d u c t . t i t le %x/h3>
<%= s a n it iz e ( p r o d u c t .d e s c r ip t io n ) %>
<div c la s s = "p r ic e _ lin e " >
<span c la s s = "p ric e ''x % = n u m b er_to _cu rre n cy(p ro d u ct. p r ic e ) %x/span>
► <%= b u tto n _to 'Add to C a r t ', lin e _ it e m s _ p a t h (p r o d u c t_ id : p ro d u c t),
► remote: tr u e %>
</div>
</div>
<% end %>

Пока мы приспособили браузер для отправки AJAX-запроса нашему приложе­


нию. Следующим шагом нужно заставить приложение вернуть ответ. Наш план
состоит в том, чтобы создать обновленный HTM L-фрагмент, представляющий
корзину, и заставить браузер вставить этот HTML в браузерное внутреннее пред­
ставление структуры и содержимого отображаемого документа, то есть в объектную
модель документа —Document Object Model (DOM). Путем воздействия на DOM
мы заставим отображение измениться прямо на глазах пользователя.
Сначала мы не дадим действию c r e a te осуществить перенаправление на ото­
бражение главной страницы, если запрос предназначается для JavaScript. Мы
сделаем это путем добавления вызова метода r e s p o n d _ to ( ), в котором ему со­
общается, что нам нужно получить ответ в формате .js.
Поначалу этот синтаксис может показаться немного странным, но это про­
сто вызов метода, которому в качестве аргумента передается необязательный
блок. Описание блоков дается в главе 4, в разделе «Блоки и итераторы». А метод
resp o n d _ to () будет более подробно рассмотрен в главе 20, в разделе «Выбор пред­
ставления данных».

ra ils3 1 / d e p o t_ l/ a p p / co n tro lle rs/ lin e _ ite m s_ co n tro lle r.rb


d ef c re a te
(Scart = c u r r e n t_ c a r t
product = Pro d u ct.fin d (p a ra m s [: p r o d u c t_ id ])
@ lin e _ite m = (S c a rt. ad d _p ro d u c t(p ro d u c t. id )
Глава 11 • Задача Е; добавление AJAX 159

respond_to do jform at|


i f @ lin e _ ite m .s a v e
fo rm a t.h tm l { r e d ir e c t _ t o s to re u r l }
► fo r m a t.js
fo rm a t.js o n { render js o n : j5)line_item ,
s t a tu s : : c re a te d , lo c a t io n : @ lin e _ite m }
e ls e
fo rm a t.h tm l { render a c tio n : "new" }
fo rm a t.js o n { render js o n : (5 )lin e _ite m .e rro rs,
s t a tu s : : u n p ro c e s s a b le _ e n tity }
end
end
end

Благодаря этому изменению, когда c r e a te завершает обработку AJAX-запроса,


Rails будет искать для отображения шаблон c r e a te te m p la te to ren d er.
Rails поддерживает шаблоны, генерирующие JavaScript, —JS означает JavaScript.
Шаблон .js.erb — это способ заставить JavaScript сделать на браузере то, что нам
нужно, и весь он создается с помощью кода Ruby на серверной стороне. Давайте на­
пишем наш первый файл: create.js.erb. Он помещается в каталог app/views/linejtems,
как и все другие представления для товарных позиции:

ra ils3 1/d epot_l/app/view s/lin e_item s/create. j s . erb


$ ( ' # c a r t ' ) . htm l("<%=j render (Scart % > ");

Этот простой шаблон заставляет браузер заменять содержимое элемента, имею­


щего идентификатор id = " c a rt" , этим кодом HTML.
Проанализируем, как это у него получается.
Для простоты и краткости библиотека j Query обозначается псевдонимом $,
с которого начинаются практически все примеры использования j Query.
Первый вызов — $( '# c a r t ’ ) —предписывает j Query найти HTM L-элемент,
имеющий id со значением c a r t . Затем вызывается метод htm l О 1, в качестве
первого аргумента которого используется нужная замена содержимого этого
элемента. Эта замена формируется за счет вызова метода re n d e r () в отношении
объекта @ cart. Вывод этого метода обрабатывается вспомогательным методом
j (), который превращает эту строку Ruby в формат, воспринимаемый как ввод
для JavaScript.
Следует заметить, что этот сценарий выполняется в браузере. На сервере вы­
полняется только та часть, которая находится между ограничителями <%= и %>.
И эго работает? В книге, конечно, это трудно показать, но все это работает.
Обязательно перезагрузите главную страницу, чтобы получить удаленную вер­
сию формы и загруженные в ваш браузер библиотеки JavaScript. Затем щелкните
на одной из кнопок Добавить в корзину (Add to Cart). Вы увидите, что корзина на
боковой панели обновится. И вы не увидите в своем браузере ни одного признака
перезагрузки страницы. Только что вы создали AJAX-приложение.

1 http://api.jquery.com/html/
160 Часть II • Создание приложения

Возможные проблемы
Удивительная простота использования AJAX в Rails не защищает от неверных ша­
гов. Поскольку мы имеем дело со свободной интеграцией ряда технологий, понять
причины неработоспособности применяемых элементов AJAX-технологии будет
непросто. Поэтому на каждом этапе мы добавляем к приложению не более одной
функции AJAX.
Если приложение Depot отказывается от демонстрации магии AJAX, можно
посоветовать выяснить следующее:
О Не нужны ли вашему браузеру какие-нибудь специальные настройки, что­
бы заставить его перезагрузить все, что есть на странице? Иногда браузеры
сохраняют в локальной кэш-памяти версию содержимого страницы, мешая
тем самым нашему тестированию. Может быть, стоит сделать полную пере­
загрузку страницы.
О Не было ли каких-нибудь сообщений об ошибках? Загляните в файл
development.log в каталоге logs. Загляните также в окно сервера Rails, по­
скольку некоторые сообщения об ошибках выводятся именно туда.
О Не видно ли при изучении регистрационного журнала входящих запросов
к действию c r e a t e ? Если их нет, значит, ваш браузер не выдает AJAX-
запросов. Если были загружены библиотеки JavaScript (при использовании
в браузере функции просмотра кода страницы будет показан код HTML), то,
может быть, в браузере отключено выполнение JavaScript?
О Некоторые читатели сообщили, что для работы корзины на основе AJAX им
пришлось остановить и снова запустить их приложения.
О Если вы пользуетесь браузером Internet Explorer, он может быть запущен
в режиме quirks mode, который обеспечивает обратную совместимость со
старыми версиями IE, но является таким же урезанным, как и они. Для
переключения Internet Explorer в стандартный режим, который работает
с компонентами AJAX гораздо лучше, в первой строке загружаемой страни­
цы должен быть соответствующий заголовок DOCTYPE. В нашем макете
используется следующий заголовок:
<!DOCTYPE html>

Заказчик вечно чем-то недоволен


Мы вполне довольны своими результатами, поскольку внесли изменения в группу
программных строк, и наше скучное приложение, созданное по стандартам Веб 1.0,
теперь оживилось благодаря скоростным возможностям AJAX-технологии из
арсенала Веб 2.0. И вот, затаив дыхание, мы приглашаем заказчика. Не говоря ни
слова, мы с гордостью щелкаем на кнопке Добавить в корзину (Add to Cart) и следим
за его реакцией, ожидая неизбежной похвалы. Но вместо этого наблюдаем лишь
его удивление и слышим вопрос: «Вы что, позвали меня продемонстрировать свою
ошибку? Ну щелкнули вы на этой кнопке, а что, собственно, изменилось?»
Глава 11 • Задача Е: добавление AJAX 161

Мы терпеливо объясняем, что на самом деле произошла масса событий, что


нужно взглянуть на корзину на боковой панели. Видите? Мы кое-что добавили,
и количество изменилось, цифру 4 сменила цифра 5.
«Понятно, —сказал он. — А я и не заметил». Ну, раз он не заметил обновления
страницы, стало быть, и другие заказчики тоже могут этого не заметить. Значит,
с пользовательским интерфейсом надо что-то делать.

11.3. Шаг ЕЗ: выделение изменений


В Rails включено несколько JavaS cript-библиотек. Одна из таких библиотек,
а именно jQuery U I1, позволяет украсить веб-страницы рядом интересных визу­
альных эффектов. Один из этих эффектов — небезызвестный (теперь уже) Yellow
Fade Technique, постепенно исчезающая желтизна. С его помощью осуществляется
подсветка элемента на странице браузера: по умолчанию элемент получает желтую
фоновую подсветку, которая постепенно сменяется белой. Эффект Yellow Fade
Technique в применении к нашей корзине показан на рис. 11.2: на заднем плане
корзина изображена в исходном варианте. Когда пользователь щелкает на кнопке
Добавить в корзину (Add to Cart) и количество товара меняется на 2, строка вспы­
хивает. Затем ее фон быстро возвращается к исходному.

Your Pragmatic Catalog

Your Pragmatic Catalog

Your Pragmatic Catalog

Your Pragmatic Catalog

Your Pragmatic Catalog


** P r a g m a tic P ro je c t A u to m at
T t..... PrapmUie Project Automation sho*
НИЦ and repeatability o f your project's p
and errors.

Simply put, we're going u> put dik thin# railed


mundane (bu* important) project stuff. That raw
tlic really exciting---and difficult- -stuff, like wr
$24.95

Рис. 11.2. Наша корзина с применением эффекта Yellow Fade Technique

Включить библиотеку jQuery UI довольно легко. Нужно просто добавить одну


строку к содержимому файла app/assets/javascripts/application.js.

1 h ttp ://jq u e ryu i.co m /


162 Часть И • Создание приложения

ra ils3 1 /d ep o t_ m / ap p /a sse ts/ ja va scrip ts/a p p lica tio n .js


// Add new D a v a S c rip t/ C o ffe e code in sep a rate file s in t h i s d ir e c t o r y and
// t h e y ' l l a u to m a tic a lly be in clu d ed in th e compiled file a c c e s s ib le from
// h ttp :/ / e x a m p le .c o m / a s s e ts / a p p lic a tio n .js I t ' s not a d v is a b le to add code
// d i r e c t l y h ere, but i f you do, i t ' l l appear a t th e bottom o f the th e
// compiled file .
//
//= re q u ire jq u e ry
► //= re q u ire jq u e ry - u i
//= re q u ire jq u e ry _ u js
//= r e q u ir e _ tr e e .

При выполнении шага A2 мы уже видели файл assets/stylesheets/application.css.


А этот файл ведет себя точно так же, но только в отношении библиотек JavaScript,
а не таблиц стилей. Обратите внимание на то, что в данной строке используется
дефис, а не символ подчеркивания, стало быть, не все авторы библиотек следуют
одним и тем же соглашениям о присвоении имен.
Давайте воспользуемся данной библиотекой для добавления к нашей корзине
этого выделения и устроим вспышку фона при каждом изменении товарной по­
зиции в корзине (как при ее добавлении, так и при изменении количества товара).
Тогда пользователям станет понятнее, что произошли какие-то изменения, даже
если не была обновлена вся страница.
Сначала нам нужно идентифицировать ту товарную позицию корзины, кото­
рая только что была обновлена. Сейчас каждая товарная позиция корзины пред­
ставляет собой простой <1:г>-элемент таблицы. Теперь нам нужно найти способ
пометить самые последние изменения. Начнем с L in e lte m s C o n tro lle r. Давайте
передадим текущую товарную позицию в шаблон, присвоив ее значение перемен­
ной экземпляра:
ra ils3 1 /d ep o t_ m / ap p /co n tro lle rs/lin e_ item s_ co n tro lle r. rb
d ef c re a te
Ifficart = c u r r e n t_ c a r t
product = Pro d u ct,fin d (p a ra m s [: p r o d u c t_ id ])
(Slin e _ite m = (S c a rt. ad d _p ro d u c t(p ro d u c t. id )
respond_to do |form at|
i f @ lin e _ ite m .s a v e
fo rm a t.h tm l { r e d ir e c t _ t o s t o r e _ u r l }
► fo r m a t.js { @ cu rren t_item = @ lin e _ite m }
fo rm a t.js o n { render js o n : |81ine_item,
s t a tu s : : c re a te d , lo c a t io n : (S)line_item }
e ls e
fo rm a t.h tm l { render a c tio n : "new" }
fo rm a t.js o n { render js o n : @ lin e _ it e m .e r r o r s ,
s t a tu s : : u n p ro c e s s a b le _ e n tity }
end
end
end

Затем в парциале JineJtem.html.erb мы проверим, не является ли отображаемая


товарная позиция той самой, в которой только что произошли изменения. Если это
так и есть, мы пометим ее с помощью атрибута id со значением c u rre n t_ ite m :
Глава 11 • Задача Е: добавление AJAX 163

rails31/depot_m /app/view s/line_ite*s/_line_item .htm l. erb


►<% i f lin e _ it e m == @ cu rren t_item %>
►<tn id = "cu rre n t_item ">
►<% e ls e %>
►<tr>
►<% end %>
<td><%= lin e _ it e m .q u a n t it y %>&times;</td>
<td><%= lin e _ it e m . p r o d u c t . t it le %></td>
<td c la s s = "it e m _ p r ic e "><%= n u m b e r_ to _ c u m e n c y (lin e _ ite m .to ta l_ p ric e ) %></td>
</tr>

В результате этих двух незначительных изменений элемент < tr > той товарной
позиции, которая подверглась в корзине самым последним изменениям, будет по­
мечен атрибутом id = "c u rre n t_ ite m ". Теперь осталось только заставить JavaScript
изменить фоновый цвет на тот, который сразу бросался бы в глаза, а затем посте­
пенно вернуть его к прежнему состоянию.
Это будет сделано в уже существующем шаблоне create.js.erb:

rails31/depot_m /app/view s/line_ite«s/create. j s . erb


$ ( '# c a r t ' ) . h tm l( ” <%=j render @ cart % > ");

► $ ('# c u r r e n t _ it e m ') , c s s ( { ' b ackground-color' : '# 8 8 ff8 8 1} ) .


►an im ate( { ' b ackgro und-co lor':'# 1 1 4 4 1 1 '}, 1000);

Вы заметили, как мы идентифицировали элемент браузера, к которому захотели


применить эффект, путем передачи аргумента '# c u rre n t_ ite m ' функции $? Затем
мы вызвали метод c ss () чтобы установить начальный цвет фона, а затем вызвали
метод an im a te () для постепенного возвращения к исходному цвету, используемо­
му нашим макетом за период в 1000 мс, широко известный как одна секунда.
После внесения этих изменений щелкните на кнопке Добавить в корзину (Add to
Cart), и тогда вы увидите, как измененная товарная позиция вспыхивает светло-
зеленым цветом, а затем постепенно возвращается к прежнему состоянию, сливаясь
с общим фоном.

11.4. Шаг Е4: предотвращение


отображения пустой корзины
От нашего заказчика поступил еще один запрос. На данный момент корзина в бо­
ковой панели отображается постоянно, даже если в ней ничего нет. Нельзя ли на­
строить ее так, чтобы она появлялась только в том случае, если в ней что-то есть?
Конечно, можно!
У нас есть из чего выбирать. Проще всего, наверное, включить HTM L-код
корзины только в том случае, когда в ней что-нибудь есть. Все это можно сделать
внутри парциала_сa r t:
► <% u n less c a r t . lin e it e m s .e m p t y ? %>
<div c la s s = " c a r t _ t it le " > Y o u r Cart</div>
164 Часть И • Создание приложения

<table>
<%= r e n d e r (c a r t .lin e _ it e m s ) %>

< tr c la s s = " t o t a l_ lin e " >


<td colspan="2">Total</td>
<td c la s s = "to ta l_ c e ll"> < % = n u m b e r _ to _ c u r r e n c y (c a r t.to ta l_ p r ic e ) %></td>
</tr>
</table>

<%= b u tto n _to 'Empty c a r t ' , c a r t , method: : d e le te ,


confirm: 'A re you s u r e ? ' %>
► <% end %>

Несмотря на то что все это работает, пользовательский интерфейс получается


грубоватым: при переходе от пустой корзины к полной происходит перерисовка
всей боковой панели. Поэтому лучше этим кодом не пользоваться. Давайте сделаем
все более изящным способом.
Библиотека jQ uery UI также предоставляет переходной эффект появления
элемента. Давайте воспользуемся аргументом b l in d метода sh o w (), который
плавно покажет корзину, сдвинув вниз все остальное содержимое боковой панели
и освободив для нее место.
Неудивительно, что для вызова эффекта мы опять воспользуемся уже имею­
щимся у нас шаблоном .js.erb. Поскольку шаблон create вызывается только при
добавлении в корзину каких-нибудь товарных позиций, мы знаем, что должны
показать корзину в боковой панели именно тогда, когда в ней точно есть одна то­
варная позиция (потому что это означает, что до этого корзина была пуста и, стало
быть, скрыта). И, поскольку корзина должна быть видна до эффекта выделения
товарной позиции, мы добавим код показа корзины перед кодом, переключающим
цвет фона товарной позиции.
Теперь шаблон имеет следующий вид:

rails31/depot_n/app/views/line_items/create. j s . erb
► if ( $ ( ’# c a rt t r ') . l e n g t h == 1) { $ ( ' # c a r t ' ) . show (' b l i n d ' , 1000); }

$ ( '# c a r t ').h t m l(" < % = j render @ cart % > ");

$ ( '# c u r r e n t _ it e m ') . c s s ( { ' b ackgro und-co lor' : '# 8 8 ff8 8 ' } ) .


a n im a te ({'b a c k g r o u n d - c o lo r':'# 1 1 4 4 1 1 '}, 1000);

Нам также нужно устроить все так, чтобы скрыть корзину, когда она опустеет.
Это можно сделать двумя основными способами. Один из них проиллюстрирован
кодом в начале этого раздела и заключается в том, чтобы вообще не генерировать
никакого кода HTML. К сожалению, если мы так и сделаем, то при добавлении
в корзину какого-нибудь товара и внезапного создания HTML-кода корзины мы
будем наблюдать, как корзина на миг появится в браузере при первоначальном
отображении, а затем исчезнет, чтобы медленно появиться снова под воздействием
эффекта b lin d .
Лучше решить эту проблему, создав HTML-код корзины и установив при этом
CSS-стиль d is p la y : none, если корзина еще пуста. Для этого нам нужно внести
Глава 11 • Задача Е: добавление AJAX 165

изменения в макет application.html.erb, который находится в каталоге app/views/


layouts. Наша первая попытка будет иметь следующий вид:
<div id = "c a r t "
<% i f (fflcart. lin e _ ite m s .e m p ty ? %>
s t y le = " d is p la y : none"
<% end %>
>
<%= re n d e r(@ c a rt) %>
</div>

Этот код добавляет к тегу <div> CSS-атрибут s ty le = , но только в том случае,


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

Вспомогательные методы
Когда мы хотим удалить какой-то процесс обработки из представления (из любого
вида представления), нам нужно написать вспомогательный метод.
Если вы взглянете на каталог арр, вы увидите в нем шесть подкаталогов.
depot> d i r app /w
[.] [..] [a s s e t s ] [ c o n t r o ll e r s ] [h e lp e r s ]
[m a ile r s ] [m odels] [v ie w s ]

Вполне очевидно, что наши вспомогательные методы находятся в каталоге


helpers. Заглянув в этот каталог, вы увидите, что в нем уже содержатся несколько
файлов:
depot> d i r app \helpers /ы
a p p lic a tio n _ h e lp e r .r b lin e _ it e m s _ h e lp e r .r b s t o r e _ h e lp e r .r b
c a r t s _ h e lp e r .r b p ro d u c ts _ h e lp e r.rb

Генераторы Rails автоматически создали файл вспомогательных методов для


каждого из наших контроллеров ( p r o d u c ts и s t o r e ) . Сама команда Rails (та,
которая изначально создала приложение) создала файл application_helper.rb. Если
хотите, можете разместить свои методы внутри файлов вспомогательных методов
соответствующих контроллеров, но, поскольку данный метод будет использован
в макете приложения, давайте поместим его в файл вспомогательных методов,
общих для всего приложения.
Давайте напишем вспомогательный метод по имени h id d e n _ d iv _ if (), кото­
рый получает условие, дополнительный набор атрибутов и блок. Он помещает
вывод, сгенерированный блоком в теге <div>, добавляя стиль d i s p l a y : none,
если условие выполняется. В макете магазина его нужно использовать следую­
щим образом:
166 Часть II • Создание приложения

rails31/depot_n/app/view s/layouts/application. htm l. erb


<%= h id d e n _ d iv _ if((fflc a rt. lin e _ ite m s .e m p ty ?, id : 'c a r t ') do %>
<%= rend er @ cart %>
<% end %>

Контроллеру магазина созданный нами вспомогательный метод будет виден


благодаря тому, что мы добавим его в файл application_helper.rb в каталоге арр/
helpers:
rails31 /d ep ot_ n/ap p /helpers/ap p lication_ h elp er.rb
module A p p lic a tio n H e lp e r
►def h id d e n _ d iv _ if(c o n d it io n , a t t r ib u t e s = { } , & block)
► i f c o n d itio n
► a t t r i b u t e s [ " s t y l e " ] = " d is p la y : none"
► end
► c o n t e n t _ t a g (" d iv " , a t t r ib u t e s , S b lo c k )
►end
end

Этот код использует стандартный вспомогательный метод Rails по имени


c o n te n t_ ta g (), который может использоваться для помещения вывода, создаваемо­
го блоком в тег. Используя форму записи &block, мы заставляем Ruby передать блок,
предоставленный h id d e n _ d iv _ if (), во вспомогательный метод c o n te n t_ ta g ( ).
И наконец, нам нужно избавиться от флэш-сообщення, которое использовалось
нами, когда пользователь очищал корзину. Теперь необходимость в нем отпала, по­
скольку корзина полностью исчезает из боковой панели при перерисовке страницы
каталога. Но есть и еще одна причина для его удаления. Теперь, когда мы используем
AJAX для добавления товара в корзину, основная страница не подвергается перери­
совке между запросами в процессе покупок. Значит, сообщение о том, что корзина
пуста, будет оставаться даже при том, что мы показываем корзину в боковой панели.
ra ils3 1 / d e p o t_ n / a p p / co n tro lle rs/ ca rts_ co n tro lle r. rb
d ef d e stro y
@ cart = c u r r e n t_ c a r t
(S c a rt. d e stro y
s e s s i o n [ : c a r t _ id ] = n i l
respond_to do | form at|
► fo rm a t.h tm l { r e d ir e c t _ t o s t o r e _ u r l }
fo rm a t.js o n { head :ok }
end
end

Теперь, добавив эти AJAX-усовершенствования, продолжим работу.


Хотя может показаться, что проделан довольно большой объем работы, на самом
деле все сделанное умещается в двух основных шагах. Во-первых, мы заставили
корзину прятаться и появляться, сделав CSS-стиль d is p la y изменяющимся в за­
висимости от количества товарных позиций в корзине. Во-вторых, мы установили
инструкции JavaScript, предназначенные для вызова эффекта b lin d , когда корзина
превращается из пустой в содержащую одну товарную позицию.
Пока что все эти изменения касались внешнего вида, но не функциональности
приложения. Давайте приступим к изменению поведения самой страницы. Как
Глава 11 • Задача Е: добавление AJAX 167

насчет того, чтобы сделать восприимчивыми к щелчкам сами изображения с до­


бавлением представляемого ими товара в корзину? Оказывается, сделать это с по­
мощью J Query совсем нетрудно.

11.5. Шаг Е5: придание изображениям


восприимчивости к щелчкам
Все, что делалось до сих пор, было реакцией на щелчок, и только на тех объектах
интерфейса, которые реагировали на него по определению (а именно, на кнопках
и ссылках). В данном случае нам нужно обработать событие onC lick, относящее­
ся к изображению, и добиться в процессе этой обработки выполнения некоторых
определяемых нами действий.
Иными словами, нам нужно получить сценарий, который выполняется при за­
грузке страницы, и заставить его найти все изображения и связать логику с этими
изображениями, чтобы направить обработку события щелчка на кнопку Добавить
в корзину (Add to Cart) для той же самой записи.
Сначала освежим в нашей памяти организацию рассматриваемой страницы:

rails31/depot_n/app/view s/store/index. htm l. erb


<% i f n o tic e %>
<p id="notice"><%= n o tic e %></p>
<% end %>

<hl>Your Pragm atic Catalog</hl>

<% (Sproducts. each do | product | %>


<div cla s s= "e n try''>
<%= im ag e _tag (p ro d u ct. im a g e _u rl) %>
<h3><%= p r o d u c t . t i t le %></h3>
<%= s a n it iz e ( p r o d u c t . d e s c r ip tio n ) %>
<div c la s s = "p r ic e _ lin e " >
<span c la s s = " p r ic e " ><%= n u m b er_to _cu rren cy(p ro d u ct. p r ic e ) %></span>
<%= b u t t o n t o 'Add to C a r t ', lin e _ it e m s _ p a t h (p r o d u c t_ id : p ro d u c t),
remote: tr u e %>
</div>
</div>
<% end %>

Используя эту информацию, мы продолжим работу, изменяя содержимое файла


app/assets/javascripts/store.js.coffee:

ra ils3 1 / d e p o t_ n / a p p / a sse ts/ ja v a sc rip ts / sto re .js.c o ffe e


# P la c e a l l th e b eh avio rs and hooks re la te d to th e matching c o n t r o lle r h ere.
# A l l t h i s lo g ic w i l l a u to m a tic a lly be a v a ila b le in a p p lic a t io n . j s .
# You can use C o ff e e S c r ip t in t h i s file : h ttp :/ / ja s h k e n a s .g ith u b .c o m / c o ffe e - s c rip t/
►$ ->
► $ ( ' . s to re .e n t r y > i m g ') . c l i c k ->
► $ ( t h i s ) . p a r e n t ( ) .fin d (' : su b m it' ) , c l i c k ( )
168 Часть II • Создание приложения

CoffeeScript1 —это еще один препроцессор, облегчающий написание активных


компонентов. В данном случае CoffeeScript помогает выразить JavaScript в более
сжатой форме. В сочетании с JQuery вы можете получить в результате весьма су­
щественные эффекты, приложив для этого незначительные усилия.
В данном случае нам в первую очередь нужно определить функцию, выполняе­
мую при загрузке страницы. Именно это и делается в первой строке сценария: в ней
определяется функция с использованием оператора ->, которая затем передается
функции по имени $, которая, как уже говорилось, является псевдонимом jQ u e r y .
Это все, что нужно, чтобы заставить j Q u e ry спланировать выполнение этих сце­
нариев после завершения загрузки страницы.
Вторая строка задает поиск всех непосредственных дочерних изображений для
элементов, определенных с атрибутом c l a s s =" e n t r y " , которые, в свою очередь,
являются потомками элемента с атрибутом c l a s s =" s t o r e " . Эта последняя часть
играет особую роль, поскольку, как и в случае с таблицами стилей, Rails будет по
умолчанию объединять все компоненты JavaScript в единый ресурс. Для каждого
найденного изображения —а таких изображений при запуске в отношении других
страниц нашего приложения может и не быть —определяется функция, связанная
с событием щелчка на этом изображении.
Третья и последняя строка обрабатывает это событие щелчка. Она начинается
с элемента, на котором происходит событие, а именно с элемента t h i s . Затем
осуществляется поиск родительского элемента, который будет контейнером d i v
с атрибутом c l a s s =" e n t r y " . Внутри этого элемента мы находим подчиненную
кнопку и переходим к щелчку на ней.
При обработке в браузере страница по внешнему виду ничем не отличается от
той, что показана на рис. 11.1. Но ведет она себя по-другому. Щелчок на изобра­
жении приводит к добавлению товара в корзину. Удивительно то, что все это было
выполнено с помощью всего лишь трех строк кода.
Разумеется, можно было все выполнить и непосредственно в JavaScript, но для
этого понадобились бы пять дополнительных наборов круглых скобок, два набора
фигурных скобок и в целом примерно на 50% больше символов. И это всего лишь
малая толика того, на что способен препроцессор CoffeeScript. Для более близ­
кого знакомства с ним есть хорошая книга «CoffeeScript: Accelerated JavaScript
Development»2.
Получается, что мы пока не уделили должного внимания тестированию. Как-
то не чувствуется, что мы сделали многое в плане функциональных изменений,
поэтому и беспокоиться об этом вроде бы не стоит. Но, ради своего спокойствия,
мы снова запускаем наши тесты:
depot> rake t e s t
Loaded s u ite
S ta rte d

1 h ttp ://ja sh k e n a s.g ith u b .c o m /co ffe e -sc rip t/


2 Trevor Burnham. CoffeeScript: Accelerated JavaScript Development. The Pragmatic
Bookshelf, Raleigh, NC and Dallas, TX, 2011.
Глава 11 • Задача Е: добавление АЗАХ 169

F in ish e d in 0.458259 seconds.


7 t e s t s , 28 a s s e r t io n s , 0 f a i l u r e s , 0 e r r o r s , 0 sk ip s
Loaded s u ite
S ta rte d

F in is h e d in 1.048274 seconds.
23 t e s t s , 26 a s s e r t io n s , 1 f a i l u r e s , 9 e r r o r s , 0 sk ip s

Надо же, отказы и ошибки. Нехорошо. Видимо, нужно снова обратиться к те­
стированию. Этим мы сейчас и займемся.

11.6. Тестирование изменений, внесенных


при добавлении AJAX
Мы посмотрели на отказы при тестировании, и увидели несколько ошибок при­
мерно следующего вида:
A ctio n V ie w : : T em plate: : E r r o r : undefined method 'lin e _ it e m s ' f o r n i l : N i l C l a s s

Поскольку эта ошибка представляет большинство отмеченных проблем, давайте


сначала займемся именно ею, чтобы потом сконцентрироваться на всех остальных
ошибках. Согласно тесту, проблема возникает при получении индекса товара, и дей­
ствительно, когда мы нацеливаем браузер на адрес http://localhost:3000/products/, то
видим окно, показанное на рис. 11.3.

НВВВВННВНН^^^Н
¥ Action Controller: Excep 1

^ 'jfg [ © localhost:3000/products

NoM ethodError in Products#index


S bowing /honm/mbys/s vw'rails4/Book/utii/work-l 92/depot/apfj/vtews/layQuts/applicatson. html. erb
where line #17 raised;

u n d e f i n e d m eth o d Jno d el_ n am e1 f o r N i l C l a s s : C l a s s

E xtracted source (around line #17):

14: < d iv id = " c o lu a in s " >


15: < d iv id = * 's id e " >
16 : < d iv id=*‘c a r t " >
17: <%= r e n d e r @ c a rt %>
18 : < / d iv >
19:
2Q: <ul>

Рис. 11.3. Ошибка в макете может повлиять на все приложение


170 Часть И • Создание приложения

Это очень полезная информация. В сообщении указывается файл шаблона,


который обрабатывался на момент возникновения ошибки (app/views/layouts/
application.html.erb), номер строки, в которой произошла ошибка, и фрагмент шабло­
на. включающий строки вокруг той, в которой возникла ошибка. Из этого можно
увидеть, что в момент возникновения ошибки вычислялось выражение @с а г t .
lin e _ ite m s и было выдано сообщение о том, что метод ' lin e _ ite m s ' для n i l не
определен (undefined method 'l in e _ i te m s ' f o r n il) .
Итак, при выводе каталога наших товаров @cart, очевидно, имеет значение n il.
И это вполне логично, поскольку значение для этой переменной задается только
в контроллере магазина. Эту ошибку довольно просто исправить, для этого нужно
лишь вообще отменить отображение корзины, пока этой переменной не будет при­
своено значение:

rails31/depot_o/app/view s/layouts/application. htm l.erb


►<% i f @ cart %>
<%= h id d e n _ d iv _ if(@ c a r t .lin e _ it e m s .e m p ty ? , id : ' c a r t ' ) do %>
<%= render (Scart %>
<% end %>
►<% end %>

После исправления этой ошибки мы перезапускаем тесты и видим, что коли­


чество ошибок сократилось до одной. Значение перенаправления было не тем, что
ожидалось. Это произошло при создании товарной позиции. И действительно, мы
внесли изменения в разделе 11.1, в подразделе «Смена направления». В отличие
от последнего изменения, которое имело случайный характер, это изменение было
преднамеренным, поэтому мы обновим соответствующие данные функционального
теста:

ra ils3 1 /d e p o t_ o / te st/fu n ctio n a l/lin e _ ite m s_ co n tro lle r_ te st. rb


t e s t "should c re a te lin e _ it e m " do
a s s e r t _ d if f e r e n c e ( ’ L in e lte m .c o u n t' ) do
post : c r e a t e , p ro d u c t_id : p r o d u c ts (: r u b y ) . id
end
► a s s e r t _ r e d ir e c te d _ to sto re _p ath
end

После внесения этого изменения наши тесты опять стали успешно проходить.
Только представьте себе, что могло бы произойти. Изменение в одной части прило­
жения в целях поддержки новых требований нарушает функцию, ранее реализован­
ную в другой части приложения. При невнимательной работе это может произойти
в таком небольшом приложении, как Depot. Но даже при весьма аккуратной работе
такое может случиться и в большом приложении.
Но на этом наша работа еще не завершена. Мы не протестировали ни одно
из наших AJAX-добавлений, к примеру, не проверили, что произойдет, когда мы
щелкнем на кнопке Добавить в корзину (Add to Cart). Rails и здесь облегчает нам
задачу
Глава 11 • Задача Е: добавление АЗАХ 171

У нас уже есть тест для создания товарной позиции should c r e a te l i n e item ,
поэтому давайте добавим еще один тест для создания товарной позиции с помощью
AJAX: should c r e a te l i n e item v ia ajax:

ra ils3 1 /d ep o t_ o / test/fu n ctio n a l/lin e _ ite m s_ co n tro ller_ te st.rb


t e s t "should c re a te lin e _ it e m v i a a ja x " do
a s s e r t _ d if f e r e n c e ( ' L in e lte m .c o u n t' ) do
xhr :p o s t, :c r e a t e , p ro d u c t_id : p r o d u c ts (: r u b y ) . id
end

a s se rt_ re sp o n s e : success
a s s e r t _ s e le c t _ jq u e r y :h tm l, '# c a r t ' do
a s s e r t _ s e le c t ' tr # c u rre n t_ ite m t d ' , /Programming Ruby 1.9/
end
end

Этот тест отличается по своему названию, по способу вызова из теста созда­


ния товарной позиции (xhr : p o st вместо простого p o st, где xhr означает трудно
произносимый термин XM L-HttpRequest), а также по ожидаемым результатам.
Вместо перенаправления, мы ожидаем успешный ответ, содержащий запрос
на замену HTM L-кода для корзины, и в этом HTM L-коде мы ожидаем найти
строку с идентификатором c u r r e n t_ ite m , имеющую значение, совпадающее
с Program ming Ruby 1 .9 . Это достигается путем применения метода a s s e r t _
s e l e c t _ j q u e r y ( ) для извлечения соответствующего HTM L с последующей
обработкой этого HTM L через те дополнительные утверждения, которые вам
нужно применить.
И наконец, здесь был представлен CoffeeScript. Хотя тестирование кода, выпол­
няемого в браузере, не входит в круг вопросов, рассматриваемых в данной книге,
мы должны протестировать наличие разметки, от которой зависит этот сценарий.
И в этом нет ничего сложного:

ra ils3 1 / d e p o t_ o / te s t/ fu n c tio n a l/ s to re _ c o n tro lle r_ te s t. rb


t e s t "markup needed f o r s t o r e . j s . c o f f e e i s in p la c e " do
get : index
a s s e r t _ s e le c t '. s t o r e .e n t r y > im g ', 3
a s s e r t _ s e le c t '. e n t r y in p u t[ty p e = s u b m it]' , 3
end

Таким образом, если слишком активный веб-дизайнер изменит разметку


страницы, повлияв тем самым на нашу логику, мы будем предупреждены об
этом и сможем внести изменения еще до того, как код начнет применяться.
Следует заметить, что : s u b m it — это расширение CSS, относящееся только
к jQuery, в нашем тесте нам нужно лишь произвести синтаксический разбор
in p u t[ty p e = s u b m it].
Поддерживание теста на уровне, соответствующем изменениям кода, является
важной составляющей сопровождения вашего приложения. Rails облегчает эту за­
дачу. Сообразительные программисты делают тестирование неотъемлемой частью
172 Часть II • Создание приложения

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

11.7. Наши достижения


При выполнении данного шага мы добавили к нашей корзине поддержку AJAX:
0 Мы переместили корзину покупателя в боковую панель. Затем мы настроили
действие c r e a te на повторный вывод страницы каталога.
0 Для вызова действия L in e lte m s C o n tr o lle r . c r e a t e ( ) с использованием
AJAX мы воспользовались кодом rem o te : tru e .
0 Затем мы использовали ERb-шаблон, чтобы создать JavaScript-код, выпол­
няемый на стороне клиента. Чтобы обновить на странице только HTML-код
корзины, в этом сценарии используется j Query.
0 Чтобы помочь пользователю увидеть изменения, связанные с корзиной,
мы добавили эффект выделения, воспользовавшись для этого библиотекой
jQuery-UI.
0 Мы написали вспомогательный метод, скрывающий пустую корзину, и
воспользовались]Query для того, чтобы показать корзину при добавлении
в нее товара.
0 Мы написали тест, проверяющий не только создание товарной позиции, но
также и содержимое ответа, возвращаемого в результате соответствующего
запроса.
Главный урок состоит в том, что AJAX-разработку нужно вести постепенно.
Начинать следует с традиционного приложения, добавляя AJAX-функции одну за
другой. С отладкой AJAX могут возникнуть трудности, поэтому постепенное до­
бавление его функций облегчает отслеживание тех изменений, которые нарушают
работу приложения. Мы увидели, что первоначальное создание традиционного
приложения существенно упрощает поддержку обоих вариантов его поведения:
как с использованием AJAX, так и без его использования.
В завершение дадим вам пару советов. Во-первых, если вы собираетесь за­
няться широкомасштабными AJAX-разработками, вам, скорее всего, нужно будет
ознакомиться со средствами отладки JavaScript, предоставляемыми браузером,
и с имеющимися в нем средствами контроля объектной модели документов
(D O M ), такими как Firebug в Firefox, Developer Tools в Internet Explorer 8,
Developer Tools в Google Chrome, Web Inspector в Safari или Dragonfly в Opera.
И во-вторых, дополнительный модуль NoScript, имеющийся в Firefox, позволяет
проверять работу приложения с JavaScript и без него одним щелчком мыши. Не­
которые считают полезным пользоваться в процессе разработки двумя разными
браузерами, на одном из которых JavaScript включен, а на другом отключен. Как
только я добавляю какую-нибудь новую функцию, то сразу же ее «подбрасываю»
обоим браузерам, чтобы убедиться, что она работает независимо от состояния
поддержки JavaScript.
Глава 11 • Задача Е: добавление AJAX 173

Чем заняться на досуге


Попробуйте проделать все это без посторонней помощи:
О Сейчас корзина пропадает при опустошении с помощью перерисовки всего
каталога. Можете ли вы изменить приложение с целью использования вме­
сто этого b lin d -эффекта из j Query UI?
О Добавьте сразу после каждой товарной позиции корзины кнопку, щелчок на
которой будет вызывать действие, уменьшающее количество товара и уда­
ляющее его из корзины, если количество станет нулевым. Заставьте все это
работать сначала без AJAX, а потом с добавлением функций AJAX.
(Подсказки можно найти по адресу http://www.pragprog.com/wikis/wiki/RailsPlay
Time.)
Задача Ж:
оформление
покупки

Основные темы:
> связы вание табли ц внеш ним и клю чами;
> и спользование объявлений b e lo n g s _ t o , has_m any и :th ro u g h ;
> со здание ф орм на основе моделей ( fo rm _ fo r) ;
> связы ван и е ф орм, моделей и представлений;
> генерация RSS-канала с пом ощ ью метода a to m _ h e lp e r, прим ененного
в отнош ении объектов моделей.

Подведем итоги. На данный момент мы собрали воедино основную систему ведения


перечня товара, создали каталог и получили довольно привлекательную корзину
покупателя. А теперь нам нужно дать покупателю реальную возможность приоб­
рести содержимое корзины. Давайте реализуем функцию оформления покупки.
Мы не собираемся делать ничего сверхъестественного. На данном этапе все,
что нам нужно, —это получить контактную информацию покупателя и избранную
им форму оплаты товара. Использовав эти сведения, мы сформируем заказ в базе
данных. В дальнейшем будет подробнее рассмотрена работа моделей, вопросы про­
верки данных и обработки форм.

12.1. ШагЖ1: регистрация заказа


Заказ представляет собой набор товарных позиций, дополненный подробностями,
необходимыми для оформления покупки. В нашей корзине уже есть товарные
Глава 12 • Задача Ж: оформление покупки 175

позиции, lin e _ it e m s , поэтому нам нужно лишь добавить к таблице lin e _ it e m s


столбец идентификатора заказа o r d e n _ id и создать таблицу заказов o r d e r s на
основе исходных предположения о составе данных, показанных на рис. 5.3, и крат­
ких переговоров с заказчиком.
Сначала мы создадим модель заказов o r d e r и дополним таблицу lin e _ it e m s :
depot> r a i l s generate sc a ffo ld order name:string address:text \
e m a il:s trin g pay_type: strin g

depot> r a i l s generate migration add _order_id_to_line_item \


o r d e r i d : integ er

Теперь, после создания миграций мы их можем применить:


depot> rake db:migrate
== C re a te O rd e rs : m ig ra tin g =======================================
-- c r e a t e _ t a b le (: o r d e r s )
-> 0.0014s
== C re ate O rd e rs: m igrated (0.0 01 5s) ==============================
== A ddO rderldToLineltem : m ig ratin g ===============================
-- add_colum n(: lin e _ it e m s , :o r d e r _ id , :in t e g e r )
-> 0.0008s
== A ddO rderldToLineltem : m igrated (0.0 00 9s) ======================

Поскольку в базе данных нет записей для этих двух новых миграций в таблице
s c h e m a _ m ig ra tio n s , задача d b : m ig r a t e применяет к базе данных обе миграции.
Разумеется, мы можем применить их по отдельности, запустив задачу миграции
после создания отдельных миграций.

Создание формы для ввода информации


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

rails31/depot_o/app/view s/carts/_cart. htm l. erb


< d iv c l a s s = " c a r t _ t i t l e " > Y o u r C a r t < / d iv >
<table>
<%= r e n d e r (c a r t . lin e _ it e m s ) %>

< tr c la s s = " t o t a l_ lin e " >


<td colspan="2">Total</td>
<td c la s s = "to ta l_ c e ll"> < % = n u m b e r _ to _ c u r r e n c y (c a r t.to ta l_ p r ic e ) %></td>
</tr>

</table>
►<%= b u tto n _to "C h eck o ut", new _order_path, method: :g e t %>
<%= b u tto n _to 'Empty c a r t ' , c a r t , method: : d e le te ,
confirm: 'A re you s u r e ? ' %>
176 Часть И • Создание приложения

Сначала нужно проверить, есть ли что-то в корзине. Если в корзине ничего нет,
мы отправим пользователя назад в каталог, оповестим о своих действиях и немед­
ленно вернемся на исходную позицию. Тем самым пользователи не смогут непо­
средственно перейти к оформлению покупки и к созданию пустых заказов. Здесь
важную роль играет инструкция re tu r n , без нее будет получена ошибка двойного
вывода double render error, поскольку ваш контроллер попытается осуществить
и перенаправление и вывод.

ra ils3 1 / d e p o t_ o / a p p / co n tro lle rs/ o rd e rs_ co n tro lle r. rb


d ef new
@ cart = c u r r e n t_ c a r t
i f (alcart. lin e _ ite m s .empty?
r e d ir e c t _ t o s t o r e _ u r l, n o tic e : "Your c a r t i s empty"
re tu rn
end

@order = Order.new
respond_to do |form at|
fo rm a t.h tm l # n ew .h tm l.erb
fo rm a t.js o n { render jso n : @order }
end

ДЖ О СПРАШ ИВАЕТ: А ГДЕ Ж Е ОБРАБОТКА КРЕДИТНЫ Х К А Р Т ? ---------------------


В реальном мире нам, наверное, потребовалось бы, чтобы наше приложение справлялось
с коммерческой стороной оформления заказа. У нас даж е мож ет появиться желание встро­
ить в него обработку кредитных карт. Но объединение с серверными системами обработки
платежей требует больш ого объема бумажной работы и преодоления многих трудностей.
Это отвлекло бы от рассмотрения вопросов работы с Rails, поэтому мы пока собираемся о т­
ложить подробности решения этой задачи.
Мы вернемся к ней в разделе 26.1 «Обработка кредитных карт с помощ ью Active Merchant»,
где будут исследованы дополнительные модули, которые могут нам помочь в этом деле.

Мы добавим тест, требующий наличия товарной позиции в корзине r e q u ir e s


it e m in c a r t , и изменим существующий тест для получения новой товарной по­
зиции s h o u ld g e t new, чтобы гарантировать ее наличие в корзине:

ra ils3 1 / d e p o t_ o / te s t/ fu n c tio n a l/ o rd e rs _ co n tro lle r_ te st. rb


► te st "r e q u ire s item in c a r t " do
► get :new
► a s s e r t _ r e d ir e c te d _ to sto re _p ath
► a s s e rt_ e q u a l flash[ : n o t i c e ] , 'Your c a r t i s empty'
►end

t e s t "should get new" do


► c a r t = C a r t .c r e a t e
► s e s s i o n [ : c a r t _ id ] = c a r t . i d
► L in e lt e m .c r e a t e ( c a r t : c a r t , p rod uct: p r o d u c ts (: r u b y ))
Глава 12 • Задача Ж: оформление покупки 177

get :new
as se rt_re sp o n s e : success
end

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


телям форму, приглашающую их ввести информацию в таблицу o r d e r s : их имя,
почтовый и электронный адреса и способ оплаты. Следовательно, нам нужно ото­
бразить шаблон Rails, содержащий форму. Поля ввода этой формы нужно связать
с соответствующими атрибутами объекта модели Rails, поэтому нам нужно создать
в действии new пустой объект модели, чтобы дать этим полям что-нибудь, с чем
они будут работать.
Нужно выполнить обычную для HTML-форм задачу: заполнить начальными
значениями поля формы, а затем извлечь эти значения в наше приложение, когда
пользователь щелкнет на кнопке передачи данных.
Для ссылки на новый объект модели O rd e r в контроллере объявляется новая пе­
ременная экземпляра @ order. Наши действия обусловлены тем, что представление
заполняет форму, используя данные этого объекта. Само по себе это обстоятельство
не представляет интереса: поскольку модель новая, все поля будут пустыми. Но
рассмотрим какой-нибудь общий случай. Возможно, нам захочется отредактиро­
вать существующий заказ. Или пользователь может попытаться подтвердить заказ,
но его данные не пройдут проверку. В таких случаях нам захочется, чтобы данные,
существующие в модели, были показаны пользователю при отображении формы.
Передача на данной стадии пустого объекта модели сохраняет совместимость со
всеми этими случаями — представление всегда сможет иметь доступ к этому объ­
екту модели.
Затем, когда пользователь щелкнет на кнопке передачи информации, жела­
тельно, чтобы новые данные извлекались из формы и, возвращаясь контроллеру,
передавались в объект модели.
Нам повезло, поскольку Rails со всем этим довольно легко справляется. Она
предоставляет в наше распоряжение массу помощников форм. Эти помощники взаи­
модействуют с контроллером и с моделями для осуществления общего решения по
обработке формы. Перед тем как приступить к созданию окончательного варианта
формы, рассмотрим простой пример:
Строка 1 <%= fo rm _fo r (Sorder do |f | %>
2 <p>
3 <%= f . l a b e l :name, "Name:" %>
4 <%= f. t e x t _ f ie ld :name, s iz e : 40 %>
5 </p>
6 <% end %>

В данном коде есть две интересные особенности. Начнем с того, что помощник
f o r m _ f o r () в первой строке формирует стандартную HTML-форму. Но он делает
не только это. Первый аргумент, (S order, сообщает методу о переменной экзем­
пляра, которую нужно использовать при создании имен нолей и при организации
передачи значений этих полей обратно контроллеру.
Как видите, fo r m _ fo r открывает Ruby-блок (который заканчивается на шестой
строке). В этот блок можно поместить обычное содержимое шаблона (к примеру,
178 Часть И • Создание приложения

тег <р>). Но для ссылки на содержимое формы можно также использовать параметр
блока (в данном случае f ). Мы воспользовались этим в четвертой строке, чтобы
добавить в форму текстовое поле. Поскольку текстовое поле создано в контексте
fo r m _ f on, оно автоматически связывается с данными объекта (fflonden.
В этих взаимосвязях нетрудно запутаться. Важно запомнить, что для свя­
зи с моделью Rails нужно знать и имена полей, и их значения. Эту информацию
ей предоставляет комбинация fo n m _ fo n и различных помощников, предназна­
ченных для работы с нолями (таких, как t e x t _ f ie ld ) . Этот процесс показан на
рис. 12.1.

контроллер: объект модели

def edit. @order.name •"Dave"


@order * Order.find(.
end

<%= form_for @order do |f| %>


<p>
<%=f label name,
<%= f text_field name , size. 40 %>
</p>
<°o end %>

Рис. 12.1. Имена в form_for проецируются на объекты и свойства

Теперь мы можем обновить шаблон, предназначенный для формы, которая со­


бирает данные о клиенте, необходимые для оформления заказа. Его вызов будет
осуществляться действием new в контроллере заказов, поэтому файл шаблона на­
зывается new.html.erb и может быть найден в каталоге app/views/orders:

rails31/depot_o/app/views/orders/new. htm l. erb


<div class= "depot_form ">
<fieldset>
<legend>Please E n te r Your Details< /legend>
<%= render 'fo rm ' %>
</fieldset>
</div>

В этом шаблоне используется парциал _form:

rails31/depot_o/app/views/orders/_form . htm l. erb


<%= fo rm _fo r(@ o rd e r) do | f | %>
<% i f @ ord er. e r r o r s . an y? %>
<div id = "e rro r_ e x p la n a tio n ">
<h2><%= p lu r a liz e (@ o r d e r .e r r o r s .c o u n t, " e r r o r " ) %>
p ro h ib ite d t h i s ord er from being saved:</h2>
Глава 12 • Задача Ж: оформление покупки 179

<ul>
<% (S o rd e r .e rro r s .fu ll_ m e s s a g e s . each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div c la s s = "fie ld ">
<%= f . l a b e l :name %><br />
► <%= f. t e x t _ f ie ld :name, s iz e : 40 %>
</div>
<div c la s s = "fie ld ” >
<%= f . l a b e l :address %><br />
► <%= f . t e x t _ a r e a : address, rows: 3, c o ls : 40 %>
</div>
<div class= "field ">
<%= f . l a b e l :e m a il %><br />
► <%= f.e m a il_ fie ld :e m a il, s iz e : 40 %>
</div>
<div class= "field ">
<%= f . l a b e l :p ay_typ e %><br />
► <%= f . s e l e c t : p ay_typ e, Order::PAYMENT_TYPES,
► prompt: 'S e l e c t a payment method' %>
</div>
<div c la s s = "a c tio n s ">
► <%= f.s u b m it 'P la c e O rd er' %>
</div>
<% end %>

В Rails имеются помощники для разных HTML-элементов формы. В предыду­


щем коде для получения имени, а также почтового и электронного адресов клиента
использовались помощники t e x t _ f ie ld , e m a il_ f ie ld и te x t _ a r e a . Более подробно
помощники форм рассматриваются в разделе 21.2 «Генерирование форм».
Единственная сложность связана со списком выбора. Подразумевается, что
список возможных вариантов оплаты является свойством модели O rd e r. И, пока
мы этого не забыли, лучше определим в файле модели o r d e r . rb дополнительный
массив:

rails31/depot_o/app/m odels/order. rb
c la s s Order < A c tiv e R e c o rd : : Base
► PAYMENTTYPES = [ "C h eck ", " C r e d it c a rd ", "P u rch ase o rd e r" ]
end

В шаблоне этот массив вариантов оплаты передается помощнику s e le c t . Ему


также передается аргумент : prompt, добавляющий фиктивный выбор, содержащий
текст приглашения.
Добавим немного магии CSS:
ra ils3 1 /d ep ot_ o/a p p /a sse ts/stylesh e e ts/a p p lica tio n . c s s . scss
.depot_form {
fie ld s e t {
background: # e fe ;
180 Часть И • Создание приложения

legend {
c o lo r : #dfd;
background: #141;
f o n t - fa m ily : s a n s - s e r if ;
padding: 0.2em lem;
}
>
form {
la b e l {
w id th : 5em;
flo at: l e f t ;
t e x t - a lig n : r ig h t ;
padding-top: 0.2em;
m a rg in - rig h t: 0.1em;
d is p la y : b lo ck ;
}
s e le c t , te x t a r e a , in p u t {
m a rg in - le ft: 0.5em;
}
.subm it {
m a rg in - le ft: 4em;
}
br {
d is p la y : none
}
}
Теперь все готово для испытания нашей формы в деле. Добавьте что-нибудь
в свою корзину и щелкните на кнопке Оформить заказ (Checkout). Вы должны
увидеть что-нибудь похожее на окно, показанное на рис. 12.2.

ЗШШЙШШШШВШЕННВ
8оЫ«Onfcno'i

•vis;. P r a g m a t ic B o o k sh e l f

2' S?,1 00 I
кошиякзя
Ыяпи* Г
. Hrvyrartnn;r»g
Rtiti* v 9

■ CIwnkjUI 1 Dr®!
T<.feri *171.90 1
L
pa.' r,:- defect тЛ 1nettod*
' РМиэд ОпШ !

Рис. 12.2. Экран оформления заказа

Выглядит неплохо! Перед продолжением разработки давайте завершим дей­


ствие new, добавив к нему ряд проверок приемлемости. Изменим модель O rd e r для
осуществления проверки заполнения клиентом всех полей ввода.
Глава 12 • Задача Ж: оформление покупки 181

Проверим также, что способ оплаты представлен одним из допустимых зна­


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

rails31/depot_o/app/m odels/order.rb
c la s s Order < A c tiv e R e c o rd : : Base
# . ..
► v a lid a t e s :name, :a d d re ss , :e m a il, presence: tru e
► v a lid a t e s :p a y_typ e , in c lu s io n : PAYMENT_TYPES
end

Следует заметить, что в начале страницы мы уже осуществили перебор всех


значений, имеющихся в переменной @ o rd e r. e r r o r s , обеспечивающий вывод со­
общений об элементах, не прошедших проверку.
Поскольку правила проверки приемлемости данных были изменены, нам не­
обходимо соответственно изменить и тестовый стенд:
rails31/depot_o/test/fixtures/orders.ym l
# Read about fixtures a t h t t p : / / a p i. ru b y o n r a ils .o r g / c la s s e s / F ix tu r e s .h tm l

one:
► name: Dave Thomas
ad d ress: MyText
► e m a il: dave@example.org
► p ay_typ e : Check

two:
name: M yStrin g
ad d ress: MyText
e m a il: M yStrin g
p ay_typ e: M yStrin g

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

rails31 /d ep ot_ o/test/fix tu res/lin e_item s. yml


# Read about fixtures a t h t t p :/ / a p i.r u b y o n r a ils .o r g / c la s s e s / F ix t u r e s .h t m l

one:
p ro d uct: ruby
► o rd e r: one

tw o :
p ro d uct: ruby
c a r t : one
182 Часть II • Создание приложения

Можно также внести и другие изменения, но в функциональных тестах исполь­


зуется только первое из них. Чтобы эти тесты были пройдены, нам понадобится
реализовать модель.

Получение детализации заказа


Давайте создадим в контроллере действие с r e a t e (), которое должно будет делать
следующее:
1. Взять значения из формы для заполнения нового объекта модели O rder.
2. Добавить товарные позиции из нашей корзины в этот заказ.
3. Проверить и сохранить заказ. Если проверка не будет пройдена, вывести
соответствующие сообщения и дать возможность пользователю внести по­
правки.
4. Как только заказ будет успешно сохранен, удалить корзину, снова показать
страницу каталога и вывести сообщение, подтверждающее размещение за­
каза.
Сначала определим взаимосвязи и начнем с отношения товарной позиции к за­
казу:

rails31/depot_o/app/m odels/line_item .rb


c la s s L in e lte m < A c t iv e R e c o r d :: 8ase
► b elo ng s_to : ord er
b elo ng s_to : product
b elo ng s_to : c a r t
d ef t o t a l _ p r ic e
p ro d u c t. p r ic e * q u a n tity
end
end

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

rails31/depot_o/app/m odels/order. rb
c la s s Order < A c tiv e R e c o rd : : Base
► has_many :lin e _ it e m s j dependent: : d estro y
# ...
end

В конечном итоге сам метод должен приобрести следующий вид:

rails31/depot._o/app/cont r o ll e r s/orders_cont r o l l e r , rb
d ef c re a te
@order = O rd e r.n e w (p a ra m s [:o rd e r])
► (Border. a d d _ lin e _ ite m s _ fro m _ c a rt( c u r r e n t _ c a r t )

respond_to do |form at|


Глава 12 • Задача Ж: оформление покупки 183

i f (Border, save
► C a r t . d e s t r o y ( s e s s io n [ : c a r t _ id ] )
► s e s s io n [ :c a r t _ id ] = n i l
► fo rm a t.h tm l { r e d ir e c t _ t o s t o r e _ u r l, n o tic e :
► 'Thank you f o r your o r d e r .' }
fo rm a t.js o n { render js o n : (Border, s ta tu s : :c re a te d ,
lo c a t io n : (Border }
e ls e
► (Bcart = c u r r e n t_ c a r t
fo rm a t.h tm l { render a c tio n : "new" }
fo rm a t.js o n { render js o n : (B o rd e r.e rro rs ,
s t a tu s : : u n p ro c e s s a b le _ e n tity }
end
end
end

Сначала создается новый объект Order, который инициализируется данными


формы. В данном случае нам нужны все данные формы, относящиеся к объектам
заказа, поэтому из аргументов мы выбираем хэш : o r d e r (это имя мы передаем
в качестве первого аргумента помощнику fo rm _ fo r). В следующей строке кода
к этому заказу добавляются товарные позиции, хранящиеся в корзине, — предна­
значенный для этого метод мы создадим буквально через минуту.
Затем объекту заказа будет дана команда на сохранение самого себя (и его до­
черних элементов, товарных позиций) в базе данных. Попутно объект заказа вы­
полнит проверку (но до нее мы доберемся через минуту). Если сохранение пройдет
успешно, мы делаем две вещи. Сначала мы подготовимся к получению от этого же
покупателя следующего заказа, удалив корзину из сессии. Затем мы снова выведем
каталог и воспользуемся методом r e d i r e c t _ t o ( ) для отображения сообщения
с благодарностью за сделанный заказ. А если сохранить данные не удастся, мы за­
ново выведем форму оформления заказа.
В действии c r e a t e мы предполагали наличие в объекте заказа метода add_
lin e _ ite m s _ f ro m _ c a rt(), поэтому давайте сейчас его и реализуем:

rails31 /d ep ot„р/арр/m odels/order. rb


c la s s Order < A c tiv e R e c o rd : : Base
# ...
► d e f a d d _ lin e _ ite m s _ fro m _ c a rt(c a rt)
► c a r t . lin e _ ite m s .e a c h do |item |
► it e m .c a r t _ id = n i l
► lin e _ ite m s << item
► end
► end
end

Для каждой товарной позиции, переносимой нами из корзины в заказ, нам


нужно сделать две вещи. Сначала мы присвоим свойству c a r t _ id значение n i l,
чтобы товарная позиция не исчезла при удалении корзины.
Затем саму товарную позицию мы добавим к коллекции lin e _ ite m s для за­
каза. Заметьте, что нам не нужно делать ничего особенного с различными полями
внешних ключей —например, устанавливать значения столбца o rd er_ id на строки
184 Часть II • Создание приложения

товарных позиций для ссылки на только что созданную строку заказа. Rails создает
эту связь для нас, используя объявления has_many () и b e lo n g s _ to ( ), добавлен­
ные нами к моделям O rder и L ineltem . Добавление каждой новой товарной по­
зиции к коллекции lin e _ ite m s возлагает ответственность за управление ключами
на Rails.

ДЖ О СПРАШ ИВАЕТ: А НЕ ВОЗНИКНУТ ЛИ У НАС ДУБЛИКАТЫ З А К А З О В ?--------


Джо озабочен тем, что наш контроллер создает объекты модели O rder в двух действиях:
new и create. Он удивлен тем, что в результате этого в базе данных не появляются про­
дублированные заказы.
Ответ прост: действие new создает в памяти объект Order только лиш ь затем, чтобы дать
программному коду шаблона материал для работы. Когда ответ отправлен в браузер, этот
объект остается не у дел, и в конечном счете он будет поглощен сборщ иком мусора, имею­
щимся в Ruby. Он никогда не будет иметь отнош ение к базе данных.
Д ействие create такж е создает объект Order, заполняя его данными из полей формы. Этот
объект и будет сохранен в базе данных.
Итак, объекты модели выполняю т две роли: они отображ аю т данные, которые извлекаются
из базы данных и помещ аю тся в нее, но они такж е являю тся и обыкновенными объектами,
в которых содержатся какие-то данные, необходимые в процессе работы. Они обращ аю т­
ся к базе данны х только тогда, когда им это предписывается, как правило, путем вызова
метода saveQ.

Придется также изменить тест, чтобы он соответствовал новому перенаправ­


лению:

ra ils3 1 / d e p o t_ p / te s t/ fu n c tio n a l/ o rd e rs _ co n tro lle r_ te st.rb


t e s t "should c re a te o rd e r" do
a s s e r t _ d if f e r e n c e ( 'O r d e r .c o u n t ' ) do
post :c r e a te , o rd e r: @ ord er. a t t r ib u t e s
end
► a s s e r t _ r e d ir e c te d _ to sto re _p ath
end

Итак, в качестве первой проверки всех этих изменений, щелкните на кнопке


Разместить заказ (Place Order) на странице оформления заказа без заполнения
полей формы. Должна быть выведена страница оформления заказа вместе с ря­
дом сообщений об ошибках, жалующихся по поводу пустых полей, показанная на
рис. 12.3.
Если ввести какие-нибудь данные, как показано в верхней части рис. 12.4, и
щелкнуть на кнопке Разместить заказ (Place Order), мы окажемся снова на страни­
це каталога, как показано в нижней части того же рисунка. Но все ли сработало?
Взглянем на базу данных.
depot> s q lite 3 - lin e db/development. s q lite 3
S Q L ite v e rs io n 3 .7 .4
E n te r " . h e lp " f o r in s t r u c t io n s
s q lite > s e le c t * from orders;
id = 1
Глава 12 • Задача Ж: оформление покупки 185

name = Dave Thomas


address = 123 Main S t
em ail = customer@example.com
pay_type = Check
c re a te d _ a t = 2011-08-07 02:31:04.964785
updated_at = 2011-08-07 02:31:04.964785
s q lite > s e le c t * from lin e_item s;
id = 10
p ro d u ct_id = 2
c a r t _ id =
c re a te d _ a t = 2011-08-07 02:30:26.188914
updated_at = 2011-08-07 02:31:04.966057
q u a n tity = 1
p r ic e = 36
o rd e r_ id = 1
s q lite > .q u it

£r A

PRAf j Ma t ic B o o k s h e l f
Yonri art P iea sc £m ef Your Delate
2*CoffeeSorpi $7233 4 errors prohibited this order from being saved:
2<Prnowmwiu ш к
Rubyt 9 NamecsmoecHarrt*:
To:a;$17190 Aodress can’t be blank
Email cant Be Man*
[ cnoc<om Д fcmpty Pay typ« is not inciudeti n me tisc

Рис. 12.3. Полный набор! Ни одно из полей не прошло проверку

Хотя увиденное вами будет различаться номерами версий и датами (и цена


будет присутствовать только после выполнения вами упражнения, заданного в раз­
деле «Чем заняться на досуге»), вы увидите отдельный заказ и одну или несколько
товарных позиций, соответствующих вашему выбору.
186 Часть II • Создание приложения

ШШ
-1 а

P r a g m a i к, В(ЮЩ1И.<!
Y.. UI : pwasei ь1««и и
■Г.ПрГ Ш по’ ЧЭПе TJftvPTtWnp-Г
v Pruyidr unnng
sv:- ос
•<1Г / 9 АМЯЯ5Ц23 Mai-ГsT
~ 'A a 1171 90

Etna. x!iStorrer§p’agprog.c
P - - Cft«*
Piat**Order

TfiaiuM you ta*r /o.i r owlsr,

Y o u r P ra g m a tic C a t a lo g

■“ CoffeeScript
cnfTeescfspt is JavaScript oonp ngr* *? р т м в е з mi or
Jav a S crip t* functionality m a p p e d in a d e a n e i. m ots
soccmc? syntax i '1tne nrat ooo* on ?n« cxoirmg riov
Idfjguaoe. CoffeeScript q u u Trevt* B untfw ii чЬ^гл you
txw, to rvoet onto a s ролег ana p?xtt>ttty or
JavaS cript аПйе writing clearer. c l e a r » . <*ld safe; code

534.00 Ac-:- to C an

Рис. 12.4. Ввод информации для оформления заказа приводит


к выводу благодарности

Еще одно, последнее AJAX-изменение


После того как заказ принят, мы возвращаемся на страницу каталога с отображе­
нием благодарственного флэш-сообщения: «Спасибо за Ваш заказ». Если поль­
зователь продолжает закупку и на его браузере включен JavaScript, мы будем за­
полнять его корзину, расположенную на боковой панели, без перерисовки главной
страницы. Значит, флэш-сообщение так и будет оставаться на экране. Нужно, чтобы
оно исчезало с добавлением в корзину первого же товара (как это и происходит,
если JavaScript в браузере отключен). К счастью, все легко исправить: мы просто
скроем блок <div>, в котором содержится флэш-сообщение, когда товар попадет
в корзину.

ДЖ О СПРАШ ИВАЕТ: А ПОЧЕМЫ БЫЛ ВЫБРАН А Т О М ? ----------------------------------


Сущ ествует несколько разных ф орматов RSS-каналов, наиболее примечательные из них
RSS 1.0, RSS 2.0 и Atom , ставшие стандартами в 2000, 2002 и 2005 году соответственно. Все
эти три формата имею т весьма широкую поддержку. Чтобы помочь со сменой версий, неко­
торы е сайты предоставляю т несколько RSS-каналов для одного и того же сайта, но сейчас
такой подход утратил актуальность, он еще больш е запуты вает пользователей и вообщ е не
рекомендован к применению.
Глава 12 * Задача Ж: оформление покупки 187

В языке Ruby предоставляется низкоуровневая библиотека, способная производить любой


из этих форматов, а также несколько менее распространенных версий RSS. Для наилучшего
результата нужно выбрать одну из этих трех основных версий.
Среда Rails всецело настроена на использование рациональных настроек по умолчанию, и в
ней в качестве формата RSS-канала по умолчанию выбран Atom. Инженерной группой по раз­
витию Интернета (IETF) он определен для интернет-сообщества, как базовый стандартный
протокол Интернета, и Rails предоставляет помощник высокого уровня по имени atom_feed,
который берет на себя все детали, основываясь на знании используемого в Rails соглашения
относительно присваивания имен для таких вещей, как идентификаторы и даты.

rails31/depot_p/app/view s/line_item s/create. j s . erb


► $ ( " # n o t ic e " ) . h id e ( ) ;

if ( $ ( '# c a r t t n ') . l e n g t h == 1) { $ ( ' # c a r t ' ) . sh ow (' b l in d ' 1 0 0 0 ) ; }

$ ( '# c a r t ' ) . htm l("<%=j render (Slcart % > ");

$ ( ' # c u rr e n t_ ite m ' ) . c s s ( { ' background-color’ : '# 8 8 ff8 8 ' } ) .


a n im a t e ({' b ackgroun d-color' : ’ #114411'}, 1000);

Учтите, что при нашем первом посещении магазина во флэш-сообщении нечего


выводить, поэтому HTML-блок с атрибутом id, имеющим значение n o tic e , не вы­
водится. Следовательно, тег с id, имеющим значение n o tic e , отсутствует, и вызов
jQuery не соответствует ни одному элементу. Так как вызов h id e ( ) применяется
к каждому соответствующему элементу, ничего не происходит и не возникает ни­
какой проблемы. Именно этого мы и добивались, поэтому все в порядке.
После получения всех данных для оформления заказа настало время оповестить
об этом отдел заказов. Это будет сделано с помощью записей в RSS-канале, а имен­
но с применением потока ордеров в формате Atom.

12.2 Шаг Ж2: применение Atom-канала


Использование стандартного формата RSS-канала, например Atom, означает, что
вы можете туг же получить все преимущества от широкого разнообразия уже
существующих клиентов. Поскольку Rails уже знает об идентификаторах, датах
и ссылках, она может освободить вас от всяческих забот об этих мелких подроб­
ностях, и позволить вам сконцентрироваться на создании удобочитаемой сводки.
Начнем с добавления к ресурсу нового действия и с включения Atom в список
форматов, на которые мы реагируем:
ra ils3 1 /d e p o t_ p / a p p /co n tro lle rs/p ro d u cts_ co n tro lle r.rb
d e f who_bought
(Sproduct = Product .fin d (p aram s[: i d ] )
respond_to do |form at|
fo rm a t. atom
end
end
188 Часть II • Создание приложения

Добавляя f o r m a t . atom, мы заставляем Rails искать шаблон по имени who_


bo u g h t. atom . b u ild e r. Такой шаблон может использовать общую типовую XML-
функциональность, предоставляемую инструментом Builder, а также использовать
знания о RSS-формате Atom, предоставляемые помощником atom_feed:

rails31/depot_p/app/views/products/who_bought.atom.builder
atom_feed do |feed|
f e e d . t i t l e "Who bought # {| 8 p r o d u c t .t it le }"

la t e s t _ o r d e r = (Sproduct .o r d e r s .s o r t _ b y (& : u p d a te d _ a t). la s t


f e e d . updated( la t e s t _ o r d e r && la te s t_ o rd e r .u p d a te d _ a t )

(Sproduct. o rd e rs , each do |order|


fe e d . e n t r y (o r d e r ) do |e n try |
e n t r y . t i t l e "O rder # { o r d e r . id } "
e n t r y . summary ty p e : 'x h tm l' do |xhtml|
xhtm l.p "Shipped to # {o r d e r .a d d r e s s }"

x h tm l.ta b le do
x h tm l.tr do
x h tm l.th ’ P r o d u c t1
x h tm l.th ’Q u a n tity '
x h tm l.th 'T o t a l P r ic e '
end

o r d e r . lin e _ ite m s .e a c h do |item |


x h tm l.tr do
x h tm l.td ite m .p ro d u c t. t i t l e
x h tm l.td ite m .q u a n tity
x h tm l.td num ber_to_currency it e m .t o t a l_ p r ic e
end
end

x h tm l.tr do
x h tm l.th ' t o t a l ' , co lsp an : 2
x h tm l.th num ber_to_currency \
o r d e r . lin e _ it e m s .m a p ( & : t o t a l_ p r ic e ) . sum
end
end

xhtm l.p "P a id by # {o r d e r . p a y _ ty p e }"


end
e n tr y .a u th o r do |author|
entry.nam e order.name
e n tr y .e m a il o rd e r.e m a il
end
end
end
end

Дополнительные сведения, касающиеся инструмента Builder, могут быть най­


дены в разделе 25.1 «Генерация XML с помощью Builder».
Глава 12 • Задача Ж: оформление покупки 189

На общем уровне канала нам нужно предоставить лишь два блока информа­
ции: заголовок и самую последнюю дату обновления. Если заказов нет, значение
u p d a te d _ a t будет нулевым и Rails предоставит вместо этого значения текущее
время.
Затем осуществляется последовательный перебор каждого заказа, связанного
с данным товаром. Следует заметить, что прямой связи между этими двумя моде­
лями не существует. Фактически эта связь является опосредованной. У товаров
имеется много товарных позиций, а товарные позиции принадлежат заказу. Мы
можем осуществить итерацию и проследить связи, но простым объявлением о су­
ществовании связи между товарами (p ro d u c ts) и заказами (o rd e rs ) через связь
с товарными позициями ( lin e _ ite m s ) мы можем упростить наш код:

rails31/depot_p/app/m odels/product. rb
c la s s Produ ct < A c tiv e R e c o rd : : Base
has_many :lin e _ ite m s
► has_many : o rd e rs , through: :lin e _ ite m s
# .. .
end

Для каждого заказа мы предоставляем заголовок, сводку и автора. Сводка может


быть полным XHTML, и мы используем это для создания таблицы названий това­
ров, заказанного количества и общих стоимостей. За этой таблицей мы поместим
абзац, содержащий способ оплаты (pay_type).
Чтобы выполнить эту работу, нужно определить маршрут. Это действие будет
отвечать за HTTP GET-запросы и будет работать с составляющими коллекции
(иными словами, с отдельными товарами), а не со всей коллекцией сразу (что
в данном случае будет означать все товары):

rails31/depot_p/config/routes. rb
Depot: A p p l i c a t i o n . r o u t e s . draw do
re so u rce s :o rd ers

re so u rce s : lin e _ ite m s

re so u rce s :c a r t s

get "s to re / in d e x "

► re so u rce s : products do
► get :who_bought, on: :member
► end

# ...
# You can have th e ro o t o f your s i t e routed w ith "r o o t"
# j u s t remember to d e le te p u b lic / in d e x . h tm l.
# ro o t :t o => ’welcome#index'
ro o t to : ’ s to re # in d e x ' , as: 's t o r e '
# . ..
end
190 Часть II • Создание приложения

Мы сами можем испытать все это в работе:


depot> c u rl - - sile n t http://localhost:3600/products/3/who_bought.atom
<?xml v e rs io n = "1 .0 " encoding= "UTF-8"?>
<feed xm l:lang= "en-US" xmlns="http://www.w3.org/2005/Atom">
<i d >t a g : l o c a lh o s t , 2005:/products/3/who_bought</i d >
< lin k ty p e = "te x t/h tm l" h re f= "h t t p : / / lo c a lh o s t :3000" r e l= "a lte r n a te "/ >
< lin k type="application/atom-(-xml"
h r e f ="h t t p :/ / lo c a lh o s t : 3000/info/who_bought/3. atom" r e l = ''s e lf "/>
<title>Who bought Programming Ruby 1 .9 < / title >
<updated>2011-08-07T02:3 1 :04Z</updated»
<entry>
< id > ta g :lo c a lh o s t,2005:Order/l< /id>
<p u b lish e d >2011-08-07T02:3 1 :04Z</published»
<updated>2011-08-07T02:3 1 :04Z</updated>
< lin k r e l= " a lt e r n a t e " ty p e = "te x t/h tm l"
h re f= "h t t p :/ / lo c a lh o s t : 3 0 0 0 / o rd e rs /l"/>
< title > O rd e r l< / t it le >
<summary type= "xhtm l'’>
<div xmlns="http://www.w3.org/1999/xhtm l">
<p>Shipped to 123 Main St</p>
<table>

</table>
<p>Paid by check</p>
</div>
</summary>
<author>
<name>Dave Thomas</name>
<email>customer(S)pragprog. com</email >
</author>
</entry>
</feed>

Выглядит неплохо. Теперь на этот канал можно подписаться в предпочитаемом


нами средстве чтения RSS- каналов.

12.3. Шаг ЖЗ: разбиение на страницы


На данный момент у нас есть несколько товаров, несколько корзин в любой момент
времени и несколько товарных позиций в каждой корзине или в каждом заказе.
Но у нас может быть практически неограниченное количество заказов, и мы наде­
емся, что их будет много, настолько много, что вывод всех их на странице заказов
быстро сделает ее слишком большой. Выход можно найти в применении допол­
нительного модуля w ill_ p a g in a te . Этот модуль расширяет Rails, предоставляя
весьма полезную функцию.
А почему нужно использовать дополнительный модуль? Ведь прежде в Rails
1.0 эта функция была частью самой Rails. Но существовала масса конкурирующих
идей по поводу реализации и улучшения этой функции, и она была изъята, чтобы
позволить новшествам развиваться.
Глава 12 • Задача Ж: оформление покупки 191

Сначала нужно проинформировать Rails о наших намерениях использовать


дополнительный модуль. Это делается путем внесения изменений в файл Gemfile.
Следует указать, что нам нужна версия, номер которой больше или равен 3.0, по­
скольку предыдущие версии с Rails 3.1 не работают.

rails31/depot_q/Gemfile
source ' h ttp :// ru b yg e m s.o rg '

gem ' r a i l s ’ , '3 .1 .0 '

# Bundle edge R a ils in s te a d :


# gem ' r a i l s ' , : g i t => ' g i t : / / g ith u b . c o m / r a i l s / r a i l s . g i t ’

gem 's q l i t e 3 '

# Gems used o n ly f o r a s s e ts and not req u ired


# in pro d uctio n environm ents by d e f a u lt ,
group :a s s e ts do
gem ' s a s s - r a i l s ' , " ~> 3 .1 .0 "
gem ' c o f f e e - r a i l s ’ , "~> 3 .1 .0 "
gem 'u g lif ie r '
end

gem ' j q u e r y - r a i l s '

# Use u n ico rn as th e web s e rv e r


# gem 'u n ic o rn '

# Deploy w ith C ap istran o


# gem 'c a p is t r a n o '

# To use debugger
# gem ' ruby-debugl9' , :r e q u ir e => 'ruby-debug'

group : t e s t do
# P r e t t y p rin te d t e s t output
gem 't u r n ' , : re q u ire => f a ls e
end
►gem 'w i l l _ p a g i n a t e ', '~> 3 .0 '

После внесения этого изменения для установки всех наших взаимозависимостей


можно воспользоваться командой bundle:
depot> bundle i n s t a l l

В зависимости от применяемой операционной системы и настроек, эту команду,


возможно, придется запускать с правами привилегированного пользователя.
Фактически команда bundle сделает очень многое. Она проверит взаимозави­
симость всех gem-пакетов, определит работоспособную конфигурацию, а также
загрузит и установит все необходимые компоненты. Но сейчас это нас не должно
волновать, мы добавляем только один компонент и можем быть уверены, что он
будет включен в gem-пакеты, устанавливаемые упаковщиком.
192 Часть II • Создание приложения

После обновления или установки нового gem-пакета нужно выполнить еще


одно, последнее действие: перезапустить сервер. Хотя Rails делает доброе дело, об­
наруживая ваши последние изменения и поддерживая их, предсказать, что именно
должно быть сделано при добавлении или замене всего gem-пакета невозможно.
Теперь сгенерируем тестовые данные. Мы можем повторно щелкать на имею­
щихся у нас кнопках, но лучше предоставить это компьютеру. Это будут не тестовые
данные как таковые, а просто результаты неких однократных действий, которые
потом можно будет выбросить. Давайте создадим файл в каталоге сценариев script.
ra ils3 1 /d ep o t_ q / scrip t/ lo ad _ o rd ers.rb
O rd e r.tra n s a c tio n do
( 1 . . 1 0 0 ) . each do | i|
O rd e r.c re a te (:n a m e => "Customer # { i } " , :address => " # { i } Main S t r e e t " ,
:e m a il => "custom er-#{i}@ exam ple.com ", :p ay_typ e => "C h eck ")
end
end

Этот код создаст 100 заказов без товарных позиций. Если нужно, вы можете
изменить сценарий, добавив товарные позиции. Учтите, что этот код проделывает
всю эту работу в одной транзакции. Это не является каким-то строгим требованием
для данной деятельности, но зато существенно ускоряет обработку.
Обратите внимание на отсутствие каких-либо инструкций r e q u ir e или ини­
циализаций для открытия или закрытия базы данных. Мы позволим Rails поза­
ботиться об этом за нас:
r a ils runner sc rip t/ lo a d _ _ o rd e rs . rb

Теперь, когда все уже установлено, мы готовы внести необходимые изменения


в наше приложение. Сначала мы изменим наш контроллер для вызова метода
p a g in a te (), передав ему страницу и указав порядок, в котором нужно отобразить
результаты:
ra ils3 1 / d e p o t_ q / a p p / co n tro lle rs/ o rd e rs_ co n tro lle r.rb
d ef index
► (Sorders = O rd e r.p a g in a te page: param s[: p ag e], o rd e r: 'c r e a t e d _ a t d e s c ',
► per_page: 10

respond_to do |form at|


fo rm a t.h tm l # in d e x .h tm l. erb
fo rm a t.js o n { render jso n : (Sorders }
end
end

Затем мы добавим ссылки в нижнюю часть нашего представления перечня за­


казов:
rails31/depot_q/app/view s/orders/index.htm l.erb
< h l> L istin g orders</hl>

<table>
<tr>
<th>Name</th>
<th>Address</th>
Глава 12 • Задача Ж: оформление покупки 193

<th>Email</th>
<th>Pay type</th>
< th x/th>
< th x/th >
< th x/th>
</tr>

<% (So rders. each do | order| % >


<tr>
<td><%= order.nam e %></td>
<td><%= o r d e r . address %></td>
<td><%= o rd e r.e m a il %></td>
<td><%= o r d e r . pay_type %></td>
<td><%= lin k _ t o 'Sh o w ', o rd er %></td>
<td><%= lin k _ t o ' E d i t ' , e d it_ o rd e r _ p a th (o rd e r ) %></td>
<td><%= lin k _ t o 'D e s t r o y ', o rd e r, confirm: 'A re you s u r e ? '
method: :d e le te %></td>
</tr>
<% end %>
</table>

<br />

<%= lin k _ t o 'New O r d e r ', new_order_path %>


►<p><%= w ill_ p a g in a t e (Sorders %></p>

Ну вот, собственно, и все! По умолчанию на каждой странице будут показывать­


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

P r a g m a t ic B o ok shelf

Qrestioas
Listing orders
Чжпе Afldre*» Tmftil Prty type
Cus'.omor iuo loe Main street xstomsr iOQCpexarpfo con c^*ock §ngw bdx occupy
Cosier*»* <>? 99 Main Street custonm*99$4mample cam Check Slum Fdl >-s!•w
Products Cusiomor Vo Jt маг stroot customer 38(0«xampte com Chock it»»» fcai L&Eiroy
C u s lotiw- 97 97 Млл Street cu*tam«r-97@wi*mp(e cam Check Show Ftil Pest toy
. ; • V.." Customer 'Jfe Man Street customer SfcSpexampie com Check Snow Ldi Aairoy
Cu»to»n#( 9 : 9*>Man Street cum CJteck S'kw £iLi
Customer 9* iM Main street ctstomcr -J4iecxamptc.com Check iinow Ldl L&siroy
Cusiomw 93 93 MJ'. Street сикЮтм-ЭЗ&ехапдо com Check S 'w Edit 0>r>;4jv
О is'rane' 9? 92 M«n Street customer-92 mple com Check Show fd l Ou
1
Custuinwf 01 91 Mji<> Stiucl ОЮСШЮГ-Э Д UHUiIIpiw.com Oieck SMu-yvgait PubHtfy

NewOf0Qf
- p i l o u s • 2 3 * 5 £ Z f l 3 Iflllfalcat..-.

Рис. 12.5. Отображения десяти заказов из более чем ста


194 Часть И • Создание приложения

Заказчику наша работа понравилась. Мы реализовали ведение каталога товаров,


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

12.4. Наши достижения


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

Чем заняться на досуге


Попробуйте проделать все это без посторонней помощи:
О Получите представления, работающие с запросами who_bought в HTML-,
XML- и JSO N -формате. Поэкспериментируйте с включением информа­
ции о заказе в XML-представление путем визуализации ^ p r o d u c t .t o _
x m l(in c lu d e : :o rd e r s ) . Сделайте то же самое для фopмaтaJSON.
О Что произойдет, если щелкнуть на кнопке Оформить заказ (Checkout) на бо­
ковой панели, когда экран оформления заказа уже отображен? Сможете ли
вы найти способ отключения кнопки при таких обстоятельствах?
О Список возможных способов оплаты пока хранится в виде константы в клас­
се Order. Можете ли вы переместить этот список в таблицу базы данных?
Сможете ли вы по-прежнему проводить проверку приемлемости данных для
соответствующего поля?
(П одсказки можно найти по адресу http://www.pragprog.com/wikis/wiki/
RailsPlayTime.)
Задача 3: отправка
электронной почты

Основные темы:
> электронная почта;
> ком плексное тестирование.

На данный момент у нас есть веб-сайт, который будет отвечать на запросы


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

13.1. Шаг 31: отправка подтверждающих


электронных сообщений
Отправка сообщений электронной почты состоит в Rails из трех основных частей:
конфигурации отправки электронных сообщений, определения момента отправки
сообщения и указания того, что нужно сообщить. Рассмотрим эти части по очереди.
196 Часть II • Создание приложения

Конфигурация отправки электронных


сообщений
Конфигурация отправки электронных сообщений является частью среды Rails-
приложений, для чего используется блок D e p o t: :A p p l i c a t i o n . co n fig u re . Если
нужно иметь одну и ту же конфигурацию для разработки, тестирования и реальной
работы, добавьте конфигурацию в файл environment.rb в каталоге config; в противном
случае добавьте разные конфигурации в соответствующие файлы в каталоге config/
environments.
Внутри блока нужно поместить одну или несколько инструкций. Сначала нужно
решить, как вы желаете доставлять электронные сообщения:
c o n fig .a c tio n _ m a ile r.d e liv e ry _ m e th o d = :smtp | :sendm ail | :t e s t

Аргументы :sm tp и : se n d m a il используются, когда нужно, чтобы попытки


отправки электронной почты осуществляла система Action Mailer. При реальной
работе приложения вам наверняка нужно будет воспользоваться одним из этих
методов.
Установка аргумента : t e s t подходит для блочного и функционального тести­
рования, которое будет использоваться в разделе «Функциональное тестирование
отправки сообщений электронной почты». Электронная почта не будет достав­
ляться, вместо этого она будет добавляться к массиву (доступному через свойство
A c t i o n M a i l e r : : B a s e . d e l i v e r i e s ) . В среде тестирования этот метод доставки
используется по умолчанию. Интересно, что в среде разработки по умолчанию
используется метод доставки : smtp. Если нужно, чтобы Rails доставляла электрон­
ную почту в процессе разработки вашего приложения, такая установка вполне по­
дойдет. Если нужно отключить доставку электронной почты в режиме разработки,
отредактируйте файл development.rb в каталоге config/environments, добавив к нему
следующие строки:
D epot: A p p l i c a t i o n . configure do
config. a c tio n _ m a ile r.d e liv e ry _ m e th o d = :t e s t
end

Настройка : s e n d m a il возлагает доставку электронной почты на почтовую про­


грамму вашей локальной системы, которая предположительно находится в каталоге
/usr/sbin. Этот механизм доставки не обладает достаточной переносимостью, по­
скольку в других операционных системах sendmail не всегда устанавливается в этот
каталог. Эта настройка также зависит от поддержки вашей локальной программы
sendmail ключей командной строки - i и - t .
Можно добиться большей переносимости, если оставить для этой настройки
ее значение по умолчанию : smtp. При этом нужно также указать некоторые до­
полнительные настройки, сообщающие Action Mailer, где найти SM TP-сервер
для обработки ваших исходящих сообщений электронной почты. Это может быть
машина, запустившая ваше веб-приложение, или может быть отдельный блок (воз­
можно, у вашего интернет-провайдера, если вы запустили Rails вне корпоративной
среды). Настройки этих параметров могут быть предоставлены вашим системным
Глава 13 • Задача 3: отправка электронной почты 197

администратором. Вы можете также определить их из своих собственных настроек


клиента электронной почты.
Следующие настройки являются типовыми для Gmail. Если нужно, можете
взять их за основу.
Depot: A p p lic a tio n .c o n fig u re do
config. a c t io n _ m a ile r . delivery_m etho d = :smtp

config. a c tio n _ m a ile r. sm tp _settin g s = {


add ress: "sm tp .gm ail.com ",
p o rt: 587,
domain: "d o m a in .o f. se n d e r. n e t" ,
a u t h e n t ic a t io n : "p la in ",
user_name: "d a v e ",
password: "s e c re t",
e n a b le _ s t a r t t ls _ a u t o : tru e
}

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

Отправка сообщений электронной почты


Завершив настройку конфигурации, давайте напишем код для отправки сообще­
ний электронной почты.
Теперь уже вы вряд ли удивитесь, узнав, что в Rails есть сценарий-генератор
для создания почтовых отправлений. Удивительным может быть то, где она их
создает. 1(оптовая система в Rails является классом, который хранится в каталоге
app/mailers. Он содержит один или несколько методов, каждый из которых соот­
ветствует определенному шаблону сообщения электронной почты. Для создания
тела сообщения эти методы по очереди используют представления (точно так же,
как действия контроллера используют представления для создания HTML и XML).
Итак, давайте создадим почтовую систему для нашего интернет-магазина. Она
будет использоваться для отправки двух разных типов электронных сообщений:
одного при размещении заказа, а другого при отправке заказа. Команда r a i l s
g e n e ra te m a ile r получает имя почтового класса, а также имена методов, относя­
щихся к действиям по отправке электронной почты:
depot> r a i l s g en erate m a ile r O rd erN o tifier re c e iv e d shipped
c re a te a p p / m a ile rs / o rd e r_ n o tifie r. rb
invoke erb
c re a te a p p /view s/o rd er_n o tifier
c re a te a p p /view s/ o rd er_n o tifier/ r e c e iv e d . t e x t . erb
c re a te ap p /vie w s/ o rd e r_n o tifie r/ sh ip p e d . t e x t . erb
invoke t e s t _ u n it
c re a te t e s t / f u n c t io n a l/ o r d e r _ n o t if ie r _ t e s t . rb

Заметьте, что мы создали класс O rd e rN o tifie r в app/mailers и два файла ша­


блонов, по одному для каждого типа электронных сообщений, в каталоге арр/
198 Часть II • Создание приложения

views/notifier. (М ы также создали файл теста, который рассмотрим чуть позже


в разделе <■Функциональное тестирование отправки сообщений электронной
почты».)
Каждый метод в почтовом классе отвечает за настройку среды для отправки кон­
кретного сообщения электронной почты. Перед тем как перейти к деталям, рассмо­
трим пример. Это код, который был сгенерирован для нашего класса O rderN otifier
с одним изменением установки по умолчанию:

rails31/depot_q/app/m ailers/order_notifier. rb
c la s s O rd erN o tifier < A c t io n M a ile r :: Base
► d e fa u lt from: 'Sam Ruby <depot@example.com>'

# S u b je c t can be se t in your I18n file a t con fig /locales/en .ym l


# w ith th e fo llo w in g lookup:
#
# e n .o r d e r _ n o tifie r . re c e iv e d .s u b je c t
#
d e f re c e iv e d
^ g re e tin g = "H i"

m ail to : " to@ exam ple.org"


end
# S u b je c t can be s e t in your I18n file a t co n fig /lo cales/en .ym l
# w ith th e fo llo w in g lookup:
#
# e n .o rd e r_ n o tifie r.s h ip p e d . s u b je c t
#
d ef shipped
@ greeting = "H i"

m ail to : " to@ exam ple.org"


end
end

Если вам показалось, что это похоже на контроллер, то в принципе так оно
и есть. Каждому действию соответствует один метод. Вместо вызова метода ото­
бражения вызывается метод отправки электронной почты. Этот метод принимает
ряд аргументов, включая :to (как показано в листинге), :сс, :from и : s u b je c t,
каждый из которых вызывает выполнение намного большего количества полез­
ных действий, чем вы могли бы ожидать. Значения, общие для всех вызовов m ail
в почтовом классе, могут быть установлены как значения по умолчанию, как это
сделано для : from в самом начале определения этого класса. Вы можете перекроить
этот класс под свои собственные нужды.
Комментарии, имеющиеся в классе также свидетельствуют о том, что стро­
ки темы (subject) уже готовы к переводу, который будет рассмотрен в главе 15
«Задача К: локализация». А теперь мы просто будем использовать параметр
: s u b je c t.
Как и при работе с контроллерами, шаблоны содержат отправляемый текст, а ко-
троллеры и почтовые классы могут предоставлять значения, вс тавляемые в такие
шаблоны посредством переменных экземпляра.
Глава 13 • Задача 3: отправка электронной почты 199

Шаблоны сообщений электронной почты


Сценарий-генератор создал в каталоге app/views/order_notifier два шаблона сообще­
ний электронной почты, по одному для каждого действия в классе O rderN otifier.
Это обычные файлы с расширением .erb. Мы будем пользоваться ими для создания
обычных текстовых сообщений электронной почты (создание сообщений HTML-
формата будет показано позже). Как и при работе с шаблонами для создания веб­
страниц нашего приложения, в файлах содержится сочетание статического текста
и динамического содержимого. Шаблон, содержащийся в файле received .text.erb, мы
можем переделать под свои потребности. Это сообщение электронной почты будет
отправляться для подтверждения заказа:

rails31/depot_q/app/view s/order_notifier/received .te x t .e r b


Уважаемый(ая) <%= (Slorder. name %>

Администрация магазина Pragm atic S to re благодарит Вас за недавний за к а з.

Вы заказали следующий товар:

<%= render (Slorder. lin e _ ite m s %>

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

Парциальный шаблон, выводящий товарную позицию, форматирует отдельную


строку с указанием количества товара и его названия. Поскольку мы имеем дело
с шаблоном, нам доступны все стандартные вспомогательные методы, такие как
t r u n c a t e ( ):

rails31/depot_q/app/view s/line_item s/_line_item . t e x t . erb


<%= s p r in tf (" % 2 d x % s ",
lin e _ it e m .q u a n t it y ,
t r u n c a t e ( l in e _ i t e m . p r o d u c t . t i t le , le n g th : 5 0 )) %>

Теперь мы должны вернуться обратно и заполнить метод re c e iv e d () в классе


O rderN otifier:

ra ils31 /d epot_r/app/m ailers/order_notifier.rb


d e f re c e iv e d (o r d e r )
(Slorder = ord er

m ail to : o rd e r .e m a il, s u b je c t: 'Подтверждение заказа в Pragm atic S t o r e '


end

Здесь мы добавили o rd e r в качестве аргумента для вызова метода re c e iv e d ,


добавили код для копирования переданного аргумента в переменную экземпляра,
и обновили вызов метода m ail (), указав, куда отправлять сообщение электронной
почты и какую строку темы использовать.
200 Часть И • Создание приложения

Генерация сообщений электронной почты


После настройки шаблона и определения почтового метода мы можем использо­
вать их в наших обычных контроллерах для создания и (или) отправки сообщений
электронной почты.
ra ils3 1 / d e p o t_ r/ a p p / co n tro lle rs/ o rd e rs_ c o n tro lle r.rb
d ef c re a te
(Border = O rd e r. new(params [ : o r d e r ] )
(Border. a d d _ lin e _ ite m s _ f rom _cart ( c u r r e n t _ c a r t )
respond_to do |form at|
i f (Border, save
C a r t .d e s t r o y (s e s s io n [ : c a r t _ i d ] )
s e s s i o n [ : c a r t _ id ] = n i l
► O rd e rN o tifie r.re c e iv e d ((B o r d e r ).d e liv e r
fo rm a t.h tm l { r e d ir e c t _ t o s t o r e _ u r l, n o tic e :
'Thank you f o r your o r d e r . ' }
fo rm a t.js o n { render js o n : (Border, s t a tu s : : c re a te d ,
lo c a t io n : (Border }
e ls e
@ cart = c u r r e n t_ c a r t
fo rm a t.h tm l { render a c tio n : "new" }
fo rm a t.js o n { render js o n : (B o rd e r.e rro rs ,
s t a tu s : : u nprocessable e n t i t y }
end
end
end

Нам также нужно обновить метод shipped () точно так же, как это было сделано
с методом re c e iv e d ():

rails31/depot_r/app/m ailers/order_notifier. rb
d ef s h ip p e d (o rd e r)
(Border = ord er

m ail to : o r d e r .e m a il, s u b je c t: 'З а к а з ив Pragm atic S to re отправлен'


end

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

Доставка сообщений с составным содержимым


Кто-то предпочитает получать сообщения электронной почты в простом тестовом
формате, а кто-то —в формате HTML. Rails упрощает отправку сообщений, имею­
щих альтернативные форматы содержимого, позволяя пользователю (или клиенту
электронной почты) решать, что именно он предпочитает увидеть.
Глава 13 • Задача 3: отправка электронной почты 201

В предыдущем разделе мы создали простое текстовое сообщение электронной


почты. Файл представления для нашего действия r e c e iv e d был назван received,
text.erb. Это имя соответствует стандартному соглашению Rails по присваива­
нию имен. Мы также можем создать сообщения электронной почты в формате
HTML.
Давайте попробуем это сделать для уведомления об отправке заказа. Нам не
нужно вносить изменение в какой-либо код, нужно просто создать новый ша­
блон:

rails31/depot_r/app/view s/order_notifier/shipped. htm l. erb


<ЬЗ>Заказ из Pragm atic Order отправлен</1пЗ>
<P>
Это сообщение уведомляет Вас, что мы отправили ваш недавний за ка з:
</р>

<table>
< t r x t h colspan="2"> K-BO< /thxth> 0nHcaHne</thx/tr>
<%= render (Slo rd er.lin e_item s %>
</table>

Нам даже не нужно изменять парциал, поскольку с поставленной задачей впол­


не сможет справиться тот, что у нас уже имеется:

rails31/depot_r/app/view s/line_item s/_line_item .htm l.erb


<% i f lin e _ it e m == (5)current_item %>
<tr id = "cu rre n t_item ">
<% e ls e %>
<tr>
<% end %>
<tdx%= lin e _ it e m .q u a n t it y %>&times;</td>
<tdx%= lin e _ it e m . p r o d u c t . t it le % x / td >
<td c la s s = "ite m _ p r ic e "x % = n u m b e r _ to _ c u r r e n c y (lin e _ ite m .t o ta l_ p r ic e ) % x / td >
</tr>

Но для шаблонов сообщений электронной почты действует еще одно волшеб­


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

Функциональное тестирование отправки


сообщений электронной почты
Когда мы использовали сценарий-генератор для создания нашей почтовой систе­
мы, обслуживающей заказы, он автоматически сконструировал соответствующий
202 Часть II • Создание приложения

файл order_notifier_test.rb в каталоге приложения test/functional. Он весьма прост по


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

ra ils3 1 / d e p o t_ r/ te st/ fu n ctio n a l/ o rd e r_ n o tifte r_ te st. rb


re q u ire ' t e s t _ h e lp e r '

c la s s O rd e rN o tifierT est < A c t io n M a ile r : :T estCase


t e s t " r e c e iv e d " do
► m ail = O rd e rN o tifie r. re c e iv e d (o r d e r s ( :o n e ))
► a s s e rt_ e q u a l "Подтверждение заказа в Pragm atic S t o r e " , m a il.s u b je c t
► a s s e rt_ e q u a l [ " dave@example.org" ] , m a il.t o
► a s s e rt_ e q u a l [ " depot@example.com" ] , m a il.fro m
► a s s e r t jn a t c h /1 x Programming Ruby 1 .9 /, m a il. body. encoded
end

t e s t "sh ip p e d " do
► m ail = O rd e rN o tifie r.s h ip p e d (o rd e rs (:o n e ))
► a s s e rt_ e q u a l "З ака з из Pragm atic S to re отправлен", m a il.s u b je c t
► a s s e rt_ e q u a l [ " dave@exam ple.org" ] , m a il.t o
► a s s e rt_ e q u a l [ " depot@example.com" ] , m ail.fro m
► a s s e r t jn a t c h /<td>l&times;<\/td>\s*<td>Programming Ruby 1.9<\/td>/,
► m ail.body.encoded
end
end

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


сообщение электронной почты, и мы используем утверждение для проверки со­
ответствия динамического содержимого ожидаемому. Обратите внимание на
использование метода a s s e r t j n a t c h () для проверки только части содержимого
тела сообщения. Ваши результаты могут различаться в зависимости от того, как вы
настроили строку d e f a u l t : from в своем классе O rderN otifier.
На этой стадии мы проверили, что сообщение, которое мы намеревались создать,
правильно сформатировано, но мы не проверили, что оно отправлено, когда клиент
завершает процесс оформления заказа. Для этого мы воспользуемся комплексными
тестами.

ДЖ О СПРАШ ИВАЕТ: А НЕ МОГУ ЛИ Я ТАКЖ Е ПОЛУЧАТЬ


ЭЛЕКТРОННУЮ П О Ч Т У ? --------------------------------------------------------------------------
Action M ailer упрощ ает написание Rails-приложений, обрабатываю щ их входящую электрон­
ную почту. К сожалению , вам необходимо найти способ для извлечения соответствующ их
сообщений электронной почты из среды окружения вашего сервера и вставки их в прило­
жение — это потребует дополнительны х усилий.
Упрощ ается сама обработка сообщ ений внутри приложения. Напиш ите в вашем классе
Action M ailer метод экземпляра под названием receive(), который получает один аргумент.
Этот аргумент будет объектом Mail::Message, соответствующ им входящей электронной по­
чте. Вы можете извлекать поля, текстовое тело и (или) вложения и использовать их в своем
приложении.
Глава 13 • Задача 3: отправка электронной почты 203

Все обычные технологии перехвата входящих сообщ ений сводятся к запуску команды
с передачей ей содержимого сообщения в качестве стандартного ввода. Если мы делаем
Rails-сценарий runner командой, вызываемой при каждом поступлении электронной почты,
мы можем настроить ее на передачу этой электронной почты коду нашего приложения, за­
нимающ емуся обработкой электронной почты. Например, используя перехват, основанный
на использовании утилиты procm ail, мы можем написать правило, похожее на приведен­
ный далее пример. Используя загадочный синтаксис procmail, это правило копирует любое
входящее почтовое сообщение, чье поле темы (subject) содерж ит «Bug Report», через наш
сценарий runner:

R U BY= /opt/local/bin/ruby
TICKET_APP_DIR=/Users/dave/Work/depot
HANDLER=' in co m in g T ic k e tH a n d le r. r e c e iv e (S T D IN .r e a d )'

:0 с
* AS u b je c t : . *Bug R e p o rt.*
| cd $TICKET_APP_DIR && $RUBY runner $HANDLER

Метод класса receive() доступен всем классам Action Mailer. Он берет текст сообщения элек­
тронной почты, переводит его в объект TMail, создает новый экземпляр класса получателя
и передает объект TM ail методу экземпляра receiveQ, имею щемуся в этом классе.

13.2. Шаг 32: комплексное тестирование


приложений
Rails систематизирует тесты, разделяя их на блочные, функциональные и комплекс­
ные. Прежде чем объяснить суть комплексных тестов, давайте кратко повторим то,
что уже было рассмотрено до сих пор:
Блочное тестирование моделей
Классы моделей содержат бизнес-логику. Например, когда мы добавляем
товар в корзину, класс модели корзины осуществляет проверку с целью
определения наличия такого же товара среди уже имеющихся в корзине
товарных позиций. Если такой товар там уже имеется, то увеличивается
количественный показатель соответствующей товарной позиции, а если нет,
добавляется новая товарная позиция для данного товара.
Функциональное тестирование контроллеров
Контроллеры управляют показом. Они получают входящие веб-запросы
(обычно, пользовательский ввод), взаимодействуют с моделями для сбора
состояния приложения, а затем составляют ответ, вызывая для пользователя
отображение соответствующего представления.
Таким образом, при тестировании контроллеров мы убеждаемся в том. что
заданный запрос получает соответствующий ответ. Нам по-прежнему нужны
модели, но мы уже охватили их блочными тестами.
Следующий уровень тестирования заключается в использовании всего цикла
работы нашего приложения. Во многом это похоже на тестирование одной из исто­
рий, которую нам рассказал заказчик, когда мы только приступали к созданию кода
204 Часть И • Создание приложения

приложения. Например, мы могли услышать следующее: «Пользователь заходит


на страницу каталога магазина. Он выбирает товар, добавляя его в свою корзину.
Затем он оформляет заказ, заполняя форму заказа. Затем он отправляет данные
формы, в базе данных создается заказ, содержащий информацию о пользовате­
ле наряду с одной товарной позицией, соответствующей товару, добавленному
пользователем в корзину. Как только заказ был получен, отправляется сообщение
электронной почты, подтверждающее покупку».
Это идеальный материал для комплексного теста. Комплексные тесты имитиру­
ют продолжительный сеанс работы нашего приложения с одним или несколькими
виртуальными пользователями. Их можно использовать для отправки запросов,
отслеживания реакции, следования перенаправления и т. д.
При создании модели или контроллера Rails создает соответствующий блочный
или функциональный тест. Комплексные тесты автоматически не создаются, но
для создания такого теста можно использовать генератор.
depot> r a i l s generate in t e g r a t io n t e s t u ser_sto rie s
invoke t e s t _ u n it
c re a te t e s t / in t e g r a t io n / u s e r _ s t o r ie s _ t e s t . rb

Следует учесть, что Rails автоматически добавляет к имени теста окончание


_ te s t.
Посмотрим на сгенерированный файл:
re q u ire ’ t e s t _ h e lp e r '

c la s s U s e r S to r ie s T e s t < A c t io n C o n t r o lle r :: In te g ra tio n T e s t


fixtu res : a l l
# R eplace t h i s w ith your r e a l t e s t s ,
t e s t "th e t r u t h " do
a s s e r t tru e
end

Давайте приступим к созданию теста для нашей истории. Поскольку тести­


роваться будет только покупка товара, нам понадобятся только лишь стендовые
данные товаров — p ro d u cts.
Поэтому вместо загрузки всех стендовых данных, загрузим только одну группу:
fixtu res :p ro du cts

Теперь создадим тест по имени «покупка товара». Мы знаем, что к концу теста
нам нужно, чтобы заказ был добавлен к таблице заказов o rd e rs , а товарная пози­
ция была добавлена к таблице lin e _ ite m s , поэтому сначала давайте очистим эти
таблицы от данных. И, поскольку мы часто использовали стендовые данные о книге
по Ruby, давайте загрузим эти данные в локальную переменную:

ra ils3 1 / d e p o t_ r/ te st/ in te g ra tio n / u se r_ s to rie s_ te s t. rb


L in e lt e m .d e le t e _ a ll
O rd e r. d e l e t e _ a l l
ruby_book = p r o d u c ts (: ruby)
Глава 13 • Задача 3: отправка электронной почты 205

Давайте возьмемся за первое предложение из нашей истории: «Пользователь


заходит на страницу каталога магазина».

ra ils3 1 / d e p o t_ r/ te st/ in te g ra tio n / u se r_ s to rie s_ te s t. rb


get "/ "
a s se rt_re sp o n s e : success
a s s e rt_ te m p la te "in d e x "

Это выглядит почти как функциональный тест. Основное различие заключается


в методе g e t. В функциональном тесте мы проверяем наш контроллер, поэтому
при вызове метода g e t () мы указываем только действие. Но в комплексном тесте
мы можем путешествовать по всему приложению, поэтому нам нужно передать
этому методу относительно полный URL-адрес для контроллера и вызываемого
действия.
Следующее предложение в нашей истории гласит: «Он выбирает товар, добав­
ляя его в свою корзину». Мы знаем, что наше приложение для добавления това­
ров в корзину использует Ajax-запросы, поэтому для вызова действия мы будем
использовать метод x m l_ h ttp _ r e q u e s t( ) . Когда метод вернет управление, мы
проверим факт наличия в корзине запрошенного товара:

ra ils3 1 / d e p o t_ r/ te st/ in te g ra tio n / u se r_ s to rie s_ te s t.rb :


x m l_h ttp _req u est :p o s t, ' / lin e _ it e m s ' , p ro d u c t_id : ruby_b oo k.id
as se rt_re sp o n s e : success

c a r t = C a rt.fin d (s e s s io n [ : c a r t _ i d ] )
a s s e rt_ e q u a l 1, c a r t . lin e _ it e m s . s iz e
a s s e rt_ e q u a l ruby_book, c a r t . li n e _ it e m s [ 0 ] . product

Далее в закрученном сюжете нашей истории попадается фраза: «Затем он


оформляет заказ...». В нашем тесте это легко проверить:

ra ils3 1 / d e p o t_ r/ te st/ in te g ra tio n / u se r_ s to rie s_ te s t.rb


get "/orders/new "
a s se rt_re sp o n s e : success
a s s e rt_ te m p la te "new"

К этому времени пользователь заполнил форму заказа своими данными. Как


только это произошло и данные были отправлены, наше приложение создает за­
каз и переходит на страницу каталога. Давайте начнем со стороны HTTP, отправив
данные формы действию save_order и проверив, что мы были перенаправлены на
главную страницу. Мы также проверим, что наша корзина теперь пуста. Вспомо­
гательный тестовый метод p o s t_ v i a _ r e d i r e c t( ) генерирует post-запрос, а затем
следует любым возвращенным перенаправлениям, пока не будет возвращен ответ
без перенаправления.

ra ils3 1 / d e p o t_ r/ te st/ in te g ra tio n / u se r_ s to rie s_ te s t. rb


p o s t _ v ia _ r e d ir e c t "/ o r d e r s ",
o rd e r: { name: "Dave Thomas",
add ress: "123 The S t r e e t " ,
206 Часть И • Создание приложения

e m a il: " dave@example.com" ,


p ay_typ e: "Check" }
a s s e r t r e s p o n s e :success
a s s e rt_ te m p la te "in d e x "
c a r t = C a rt .f in d (s e s s io n [: c a r t _ i d ] )
a s s e rt_ e q u a l 0, c a r t . lin e _ it e m s . s iz e

Затем мы заглянем в базу данных и убедимся, что мы создали заказ и соответ­


ствующую товарную позицию и что в них содержатся верные сведения. Поскольку
перед началом тестирования мы очистили таблицу o rd e rs , мы просто проверим,
что теперь в ней содержится только лишь наш новый заказ:

ra ils3 1 / d e p o t_ r/ te st/ in te g ra tio n / u se r_ s to rie s_ te s t. rb ШЯЯШШШШШЯШ


o rd e rs = O r d e r . a ll
a s s e rt_ e q u a l 1, o r d e r s .s iz e
o rd e r = o rd e r s [0 ]

a s s e rt_ e q u a l "Dave Thomas", order.nam e


a s se rte q u a l "123 The S t r e e t " , o rd e r.a d d re ss
a s s e rt_ e q u a l " dave@example.com" , o rd e r.e m a il
a s s e rt_ e q u a l "C heck ", o rd e r.p a y _ty p e

a s s e rt_ e q u a l 1, o r d e r . li n e _ it e m s . s iz e
lin e _ it e m = o r d e r . lin e _ it e m s [0 ]
a s s e rt_ e q u a l ruby_book, lin e _ ite m .p ro d u c t

И наконец, мы проверим, что само почтовое отправление правильно адресовано


и имеет ожидаемую нами строку темы:

ra ils3 1 / d e p o t_ r/ te st/ in te g ra tio n / u se r_ s to rie s_ te s t.rb


m ail = A c t io n M a ile r : : B a s e . d e li v e r i e s . la s t
a s s e rt_ e q u a l [ " dave@example.com" ] , m a il.t o
a s s e rt_ e q u a l 'Sam Ruby <depot@example.com> ', m a il[:f r o m ].v a lu e
a s s e rt_ e q u a l "P rag m a tic S to re Order Confirm ation", m a il.s u b je c t

Ну вот и все.
А вот полный исходный код комплексного теста:

ra ils3 1 / d e p o t_ r/ te st/ in te g ra tio n / u se r_ s to rie s_ te s t. rb


re q u ire 't e s t _ h e lp e r '

c la s s U s e r S to r ie s T e s t < A c tio n D is p a tc h : : In te g ra tio n T e s t


fixtu res : products

# A u ser goes to th e index page. They s e le c t a pro d uct, adding i t to t h e i r


# c a r t , and check o u t, f i l l i n g in t h e i r d e t a il s on th e checkout form. When
# th e y subm it, an o rd er i s c re ated c o n ta in in g t h e i r in fo rm a tio n , along w ith a
# s in g le li n e item correspondin g to th e product th e y added to t h e i r c a r t .

t e s t "b u yin g a p rod uct" do


L in e lt e m .d e le t e _ a ll
O rd e r. d e le te _ a 11
ruby_book = p r o d u c ts (: ruby)
Глава 13 • Задача 3: отправка электронной почты 207

get "/ "


a s se rt_ re sp o n s e : success
a s s e rt_ te m p la te "in d e x "

xm l_http_nequest :p o s t, ' / lin e _ it e m s ' pnoduct_id: ruby_b o ok.id


as se rt_re sp o n s e : success

c a r t = C a r t .f in d (s e s s io n [: c a r t _ i d ] )
a s s e rt_ e q u a l 1, c a r t . li n e _ it e m s . s iz e
a s s e rt_ e q u a l ruby_book, c a r t . lin e _ it e m s [ 0 ] .product

get "/orders/new "


as se rt_re sp o n s e : success
a s s e rt_ te m p la te "new"

p o s t _ v ia _ r e d ir e c t "/ o r d e r s ",
o rd e r: { name: "Dave Thomas",
a d d re ss: "123 The S t r e e t " ,
e m a il: " dave@example.com"
p a y _ ty p e : "Check" }
a s se rt_ re sp o n s e : success
a s s e rt_ te m p la te "in d e x "
c a r t = C a rt.fin d (s e s s io n [ : c a r t _ i d ] )
a s s e rt_ e q u a l 0, c a r t . lin e _ it e m s . s iz e

o rd ers = O r d e r . a ll
a s s e rt_ e q u a l 1, o r d e r s .s iz e
ord er = o rd e rs [0 ]

a s s e rt_ e q u a l "Dave Thomas", order.name


a s s e rt_ e q u a l "123 The S t r e e t " , o r d e r . address
a s s e rt_ e q u a l "dave(8example. com" o r d e r . em ail
a s s e rt_ e q u a l "C heck", o rd e r.p a y _ty p e

a s s e rt_ e q u a l 1, o r d e r .lin e _ it e m s . s iz e
lin e _ it e m = o r d e r . Iin e _ it e m s [0 ]
a s s e rt_ e q u a l ruby_book, lin e _ ite m .p ro d u c t

m ail = A c t io n M a ile r : : B a s e . d e li v e r i e s . la s t
a s s e rt_ e q u a l ["daveiaexam ple.com "], m a il.t o
a s s e rt_ e q u a l 'Sam Ruby <depot@example.com> ', m a il[:f r o m ] .v a lu e
a s s e rt_ e q u a l "Prag m atic S to re Order Confirm ation", m a il, su b je c t
end

Все вместе взятые блочные, функциональные и комплексные тесты позволяют


вам провести гибкую проверку всех аспектов вашего приложения как по отдель­
ности, гак и в сочетании друг с другом. В разделе 26.3 «Поиск новых дополнений
в RailsPlugins.org», будет рассказано, где можно будет найти дополнительные моду­
ли, переводящие все это на следующий уровень и позволяющие создавать простые
текстовые описания поведения, которые может прочитать ваш заказчик и которые
могут быть проверены в автоматическом режиме.
208 Часть II • Создание приложения

А теперь пора завершить выполнение этого шага, переговорить с заказчиком


и выяснить, какие функциональные возможности ожидают наше приложение
Depot.

13.3. Наши достижения


Использовав совсем небольшой объем кода и всего лишь несколько шаблонов,
0 мы настроили все три среды нашего Rails-приложения: разработочную,
тестировочную и эксплуатационную — на отправку исходящих сообщений
электронной почты;
0 мы создали и настроили почтовую систему, которая будет отправлять под­
тверждающие почтовые сообщения как в виде простого теста, так и в HTML-
формате заказчикам нашего товара;
0 мы создали как функциональный тест для создания сообщений электронной
почты, так и комплексный тест, охватывающий весь сценарий, связанный
с оформлением заказа.

Чем заняться на досуге


О Добавьте к таблице o r d e r s столбец s h i p _ d a t e (дата отправки) и от­
правьте уведомление, когда его значение будет обновлено контроллером
O rd e rs C o n tro lle r.
О Обновите приложение, чтобы оно отправляло сообщение электронной почты
системному администратору —лично вам — при возникновении в приложе­
нии каких-нибудь отказов, например того отказа, который обрабатывался
в разделе 10.2 «Шаг Д2: обработка ошибок».
О Добавьте комплексный тест для обоих предыдущих элементов.
Задача И: вход
в административную
область

О сн о в н ы е тем ы :

> добавлен ие к моделям безоп асн ы х паролей;


> использование доп ол ни тельн ы х проверок;
> д об авлен ие к сессии аутентиф икации;
> и спользование консоли r a i l s ;
> и спользование транзакций базы данных;
> написание метода отката с использованием A ctive Record.

Итак, заказчик доволен —в сравнительно короткие сроки мы совместными усилия­


ми собрали воедино все необходимое для работы корзины покупателя, и он может
приступать к ее демонстрации своим пользователям. Но ему захотелось увидеть
еще одно дополнение. На данный момент получить доступ к административным
функциям может кто угодно. Он хочет, чтобы мы добавили стандартную систему
пользовательского администрирования, которая заставит вас пройти авторизацию
для входа на административную часть веб-сайта.
Похоже, переговоры с заказчиком показали, что нам незачем создавать для
приложения слишком сложную систему безопасности. Нужно лишь распознать
некоторых людей, основываясь на пользовательских именах и паролях. Прошедшие
систему опознавания получат доступ к административным функциям.
210 Часть II • Создание приложения

14.1. Шаг И1: добавление пользователей


Начнем с создания модели и таблицы базы данных, в которых будут содержаться
имена и пароли пользователей с нравами администратора. Пароли будут храниться
не в виде простого текста, а в виде цифрового хэша. Тем самым мы гарантируем,
что, даже при вскрытии базы данных, хэш не покажет исходный пароль и не сможет
использоваться для входа в административную область в качестве пользователя
с помощью форм.
aepot> r a i l s g en erate s c a f f o ld User n am e :strin g p a ssw o rd _d ig est: s t r in g

Если вы запустили выпуск Rails, отличающийся от 3.1.0, обратитесь к wiki1,


чтобы увидеть, нет ли там каких-нибудь дополнительных инструкций о том, что
нужно делать, чтобы включить метод h a s_ se c u re _ p a ssw o rd ().
Теперь, как обычно, запустите миграцию:
depot> rake db :m ig rate

Теперь мы должны конкретизировать модель пользователя — user:

rails31/depot_r/app/m odels/user. rb
c la s s User < A c tiv e R e c o rd : : Base
v a lid a t e s :name, presence: t r u e , uniqueness: tru e
has_secure_password
end

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


быть повторяющихся имен). Затем следует таинственный метод h a s _ s e c u r e _
password.
Вам ведь знакомы такие формы, которые предлагают вам ввести пароль, а затем
заставляют вас ввести его еще раз в отдельное поле, чтобы убедиться, что вы ввели
именно то, что задумали? A Rails может автоматически проверить совпадение этих
двух экземпляров паролей. Как это работает, мы увидим через несколько минут.
А теперь нужно лишь знать, что э го вполне обычная операция, и что ее поддержка
встроена в саму среду Rails.
С этим кодом у нас появляется возможность показать в форме оба поля: пароля
и его подтверждения, а также возможность провести аутентификацию пользователя
с заданным именем и паролем.

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

1 h ttp ://p ra g p ro g .co m /w ikis/w ik i/C h a n g e sT o R a ils


Глава 14 • Задача И: вход в административную область 211

Начнем с контроллера. В нем определяются стандартные методы: in d e x ( ) ,


show(), new(), e d i t ( ) , u p d a te Q и d e le t e Q . Но при работе с пользователями
нам нечего особо показывать, пользуясь методом show (), разве что имя и не­
понятный хэш пароля. Поэтому исключим перенаправление на показ данных
пользователя после операции создания. Вместо этого давайте осуществим пере­
направление на перечень пользователей и добавим имя пользователя к флэш-
уведомлению.
ra ils3 1 / d e p o t_ r/ a p p / co n tro lle rs/ u se rs_ co n tro lle r.rb
d ef c re a te
(Suser = U s e r. new (param s[: u s e r ] )
respond_to do |form at|
i f @ user.save
► fo rm a t.h tm l { r e d ir e c t _ t o u s e r s _ u r l,
► n o tic e : "Пользователь #{(2user. name} был успешно со зд ан ." }
fo rm a t.js o n { render js o n : @user,
s t a tu s : : c re a te d , lo c a t io n : @user }
e ls e
fo rm a t.h tm l { render a c tio n : "new" }
fo rm a t.js o n { render js o n : (S u s e r.e rro rs ,
s t a tu s : : u n p ro c e s s a b le _ e n tity }
end
end
end

To же самое сделаем и для операции обновления:


ra ils3 1 / d e p o t_ r/ a p p / co n tro lle rs/ u se rs_ co n tro lle r. rb
def update
(Suser = User ,fin d (p aram s[: i d ] )
respond_to do |form at|
i f @ u s e r .u p d a te _ a ttrib u te s (p a r a m s [:u s e r])
► fo rm a t.h tm l { r e d ir e c t _ t o u s e r s _ u r l,
► n o tic e : "Сведения о пользователе # {@ u ser. name} были успешно обновлены." }
fo rm a t.js o n { head :ok }
e ls e
fo rm a t.h tm l { render a c tio n : " e d i t " }
fo rm a t.js o n { render js o n : (S u s e r.e rro rs ,
s ta tu s : : u n p ro c e s s a b le _ e n tity }
end
end
end

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

ra ils3 1 / d e p o t_ r/ a p p / co n tro lle rs/ u se rs_ co n tro lle r.rb


d ef index
► (Susers = U s e r .o rd e r ( :name)
respond_to do |form at|
fo rm a t.h tm l # in d e x .h tm l.e rb
fo rm a t.js o n { render js o n : (Susers }
end
end
212 Часть II • Создание приложения

После внесения изменений в контроллер, обратимся к представлению. На дан­


ный момент представление не выводит уведомительную информацию, и в таблице
также содержится слишком много сведений. В частности, таблица показывает
оцифрованный пароль. Мы продолжим работу, добавив уведомление и удалив обе
строки, th и td , для поля оцифрованного пароля:

rails31/depot_r/app/view s/users/index.htm l.erb


< h l> L istin g users</hl>
► <% i f n o tic e %>
► <p id="notice"><%= n o tic e %></p>
► <% end %>

<table>
<tr>
<th>Name</th>
< th x/th>
< th x/th>
< th x/th>
</tr>

<% (Susers.each do |user| %>


<tr>
<tdx%= user.name % x / td >
<tdx%= lin k _ t o 'S h o w ', u ser % x / td >
<tdx%= lin k _ t o ' E d i t ' , e d it_ u s e r _ p a th (u s e r ) % x / td >
<tdx%= lin k _ t o 'D e s t r o y ', u se r, confirm: 'A re you s u r e ? ',
method: :d e le te % x / td >
</tr>
<% end %>
</table>

<br />

<%= lin k _ t o 'New U s e r ', new_user_path %>

И наконец, нам нужно обновить форму, используемую как для создания нового
пользователя, так и для обновления сведений об уже имеющемся пользователе. Сна­
чала заменим оцифрованный пароль полями пароля и подтверждения пароля. Затем
добавим теги legend и fieldset. И, в завершение, поместим вывод в тег <div> с атри­
бутом c la s s , значение которого мы ранее определили в нашей таблице стилей.

rails31/depot_r/app/view s/users/_form .htm l.erb


<div class= "depot_form ">

<%= fo rm _fo r @user do |f | %>


<% i f (S u s e r .e r r o r s . any? %>
<div id = "e rro r_ e x p la n a tio n ">
<h2x%= p lu r a liz e ((5 )u s e r.e rr o rs .c o u n t, " e r r o r " ) %>
, сведения об этом пользователе не могут быть сохранены:</h2>
<ul>
<% @ u s e r.e rro rs .fu ll_ m e s s a g e s .e a c h do |msg| %>
< lix % = msg % x / li >
<% end %>
Глава 14 • Задача И: вход в административную область 213

</ul>
</div>
<% end %>

<fieldset>
<legend>Bвeдитe сведения о пoльзoвaтeлe</legend>

<div>
<%= f . l a b e l :name %>:
<%= f . t e x t _ f ie ld :name, s iz e : 40 %>
</div>

<div>
<%= f . l a b e l
:password, 'Пароль' %>:
<%= f . password_field :passw ord, s iz e : 40 %>
</div>

<div>
<%= f . l a b e l : password_confirm ation, 'Подтверждение' %>:
<%= f . password_field : passw ord con firm atio n , s iz e : 40 %>
</div>

<div>
<%= f.s u b m it %>
</div>

</fieldset>
<% end %>

</div>

Давайте испытаем это в работе. Перейдите на адрес http://localhost:3000/users/new.


Наш выдающийся пример дизайна страницы показан на рис. 14.1.

jjjjj | Q lot»lhost:3000/users/new ф! ^

яПЕЯйш P ra g m a tic B o o k s h e lf

Hom e
Q uestions
New user
News Enter U se r Details
Contact
Nam e; j

| Password; f

Ij _____
Confirm ; f~
_I
Create U se r

Back

Рис. 14.1. Ввод сведений о пользователе


214 Часть И • Создание приложения

После щелчка на кнопке Создать пользователя (Create User) перечень выводится


еще раз с позитивным флэш-уведомлением:
depot> s q lit e 3 - lin e db/developm ent. s q lit e 3 " s e le c t * from u s e rs "
id = 1
name = dave
p a s s w o rd d ig e s t = $2a$10$lki6/oAcOW4AWg4A0e0T8uxtri2Zx5g9taBXrd4mDSDV13rQRWRNi
c re a te d _ a t = 2011-08-07 12:05:20.445775
updated_at = 2011-08-07 12:05:20.445775

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

ra ils3 1 / d e p o t_ r/ te st/ fu n ctio n a l/ u s e rs_ c o rrtro lle r_ te st. rb


re q u ire ' t e s t _ h e lp e r '
c la s s U s e r s C o n tro lle r T e s t < A c tio n C o n tro lle r ::T e s tC a s e
setup do
► @ in p u t_ a ttrib u te s = {
► name: "sam ",
► password: " p r iv a t e " ,
► passw ord_confirm ation: " p r iv a t e "
► >
@user = u s e r s (:o n e )
end
# .. .
t e s t "should c re a te u s e r" do
a s s e r t _ d if f e r e n c e ( ' U s e r. c o u n t' ) do
► post :c r e a te , u se r: @ in p u t_a t:trib u te s
end

► a s s e r t _ r e d ir e c t e a _ t o u sers_path
end
# .. .
t e s t "should update u s e r" do
► put : update, id : (8user.to_param , u ser: @ ir,p u t_ a ttrib u te s
► a s s e r t _ r e d ir e c te d _ to u sers_path
end
end

Теперь, когда мы можем управлять нашими пользователями, нам нужно сна­


чала провести аутентификацию пользователей, а затем ограничить доступ к ад­
министративным функциям, предоставив его только пользователям с правами
администратора.

14.2, Шаг И2: аутентификация


пользователей
Что означает добавление поддержки входа в административную область для адми­
нистраторов вашего магазина?
Глава 14 • Задача И: вход в административную область 215

О Нам нужно предоставить форму, позволяющую им ввести имя пользователя


и пароль.
О После входа в административную область нужно каким-то образом зафик­
сировать этот факт для всей остальной сессии (или до тех пор, пока они не
выйдут из административной области).
О Нам нужно ограничить доступ к административной области приложения,
разрешив управлять магазином только тем, кто вошел в административную
область.
Нам нужен контроллер сессии для поддержки входа в административную об­
ласть и выхода из нее, и нам нужен контроллер для работы администраторов.
depot> r a i l s generate c o n t r o lle r Sessions new create destroy
depot> r a i l s generate c o n t r o lle r Admin index

Действие S e s s io n s C o n tro lle r# c re a te должно будет сделать запись где-нибудь


в сессии, чтобы было известно, что в административную область вошел администра­
тор. Пусть в ней хранится идентификатор ее объекта User с использованием ключа
: use r_ id . Код входа в административную область имеет следующий вид:

r9 ^ 3 1 / ( | t e p o t _ r / a p p / c o n t r o lle r s / s e s s io n s _ c o iT t ^ H M H H H H H H H H H I
def c re a te
► u ser = User.find_by_nam e(param s[: name])
► i f u ser and u s e r . a u th e n tic a te (p a ra m s [: passw ord])
► s e s s io n [ : u s e r _ id ] = u s e r .id
► r e d ir e c t _ t o adm in_url
► e ls e
► r e d ir e c t _ t o lo g in _ u r l, a l e r t : "Неверная комбинация имени и пароля"
► end
end

Здесь мы делаем и кое-что новое: использование формы, которая не связана


напрямую с объектом модели. Чтобы посмотреть, как это работает, рассмотрим
шаблон для действия sessions#new :

rails3 1/d ep ot_ r/арр/view s/sessions/new. htm l. erb


<div class= "depot_form ">
<% i f flash[ : a l e r t ] %>
<p id="notice"><%= flash[ : a l e r t ] %></p>
<%.end %>

<%= form _tag do %>


<fieldset>
<legend З а р е ги стр и р уй те сь, пожалуйста< /^епй>

<div>
<%= la b e l_ t a g :name, ’ И м я:' %>
<%= te x t_ fie ld _ ta g :name, params[ : name] %>
</div>

<div>
<%= la b e l_ t a g :password, 'П ар о л ь:' %>
216 Часть II • Создание приложения

<%= passw ord_field_tag : password, param s[: password] %>


</div>

<div>
<%= subm it_tag "L o g in " %>
</div>
</fieldset>
<% end %>
</div>

Эта форма отличается от тех, которые мы видели раньше. Вместо использо­


вания метода form _for, в ней используется метод f orm_tag, который просто со­
здает обычную HTML-форму с тегами <form>. Внутри этой формы используются
te x t_ fie ld _ ta g и passw ord_field_tag, два вспомогательных метода, создающих
HTM L-теги < input> . Каждый вспомогательный метод получает два аргумента.
Первый —эго имя, которое дается полю, и второй —это значение, которым нужно
заполнить поле. Эта разновидность формы позволяет нам связать значения в струк­
туре params непосредственно с полями формы, при этом объект модели не требу­
ется. В нашем случае мы выбрали использование объекта params непосредственно
в форме. Альтернативным вариантом могло бы быть объявление контроллером
переменных экземпляра.
Здесь также используются помощники la b e l_ ta g для создания HTML-тегов
<l a b e l >. Этот помощник также получает два аргумента. В первом содержится имя
поля, а во втором — выводимая надпись.
Посмотрите на рис. 14.2 и обратите внимание на то, как значение ноля формы
передается между контроллером и представлением при помощи хэша params: пред­
ставление получает значение для отображения в поле из params [: name], и, когда
пользователь отправляет данные формы, новое значение поля делается доступным
контроллеру все тем же способом.
Если пользователь успешно вошел в административную область, мы сохраняем
идентификатор записи пользователя в данных сессии. Присутствие этого значения
в сессии будет служить признаком того, что в административную область вошел
пользователь с правами администратора.
Как вы, наверное, и ожидали, действия контроллера для выхода из администра­
тивной области будут выглядеть намного проще:

ra ils3 1 / d e p o t_ r/ a p p / co n tro lle rs/ se ss io n s _ c o n tro lle r. rb


d ef d e stro y
► s e s s io n [ : u s e r _ id ] = n i l
► r e d ir e c t _ t o s t o r e _ u r l, n o tic e : "Сеанс работы завершен"
end

И наконец, настала пора добавить страницу перечня, первый экран, который


видит администратор после входа в административную область. Давайте заставим
эту страницу приносить дополнительную пользу — показывать общее количество
заказов, имеющихся в нашем магазине. Создайте шаблон в файле index.html.erb
в каталоге app/views/admin. (В этом шаблоне используется помощник p l u r a l i z e ( ),
который в данном случае генерирует строку o rd e r или o rd e rs в зависимости от
количества, указанного в его первом аргументе.)
Глава 14 • Задача И: вход в административную область 217

<°о form_tag do %>


Name:
<%= text_field_tag :name(^params[:name]^o>

<% end °o>

Ш Ш Ш Ш Ж
def login _________ .. -'
name =ф a ra ms[:name f)

end

•' ' Adm.*


. . A d m n logrn
^o»t:MXKJ,:og-n/togn *' Q»

Рис. 14.2. Перенос аргументов между контроллерами, шаблонами и браузерами

rails31/depot_r/app/views/adm in/index. htm l. erb


<hl>flo6po пожаловать</Ь1>

Сейчас <%= Time.now %>


У нас имеется <%= p lu r a llz e (@ t o t a l_ o r d e r s , " o r d e r " ) %>.

Количество устанавливается в действии in d e x ():

ra ils3 1 /d ep ot_ r/a p p /co n tro llers/ ad m in _ con tro lle r.rb
c la s s A d m in C o n tro ller < A p p lic a tio n C o n t r o lle r
d e f index
► @ to ta l_ o rd e rs = O rd er.co u n t
end
end

Перед тем как этим воспользоваться, нам нужно выполнить еще одну задачу.
Если прежде мы зависели от генератора временной платформы, который создавал
нашу модель и маршруты для нас, то на этот раз мы просто сгенерируем контрол­
лер, поскольку для этого контроллера нет модели, основанной на базе данных. К со­
жалению, если не следовать соглашению, применяемому при генерации временных
218 Часть И • Создание приложения

платформ, Rails не может узнать, какие действия для этого контроллера отвечают
за GET-запросы, а какие отвечают за POST-запросы и т. д.
Нам нужно предоставить эту информацию путем редактирования нашего файла
config/routes, rb:
rails31/depot_r/config/routes. rb
D epot: A p p l i c a t i o n . ro u tes .draw do
► get 'adm in' => ' adm in#index'

► c o n t r o lle r :s e s s io n s do
► get 'l o g i n ' => :new
► post 'l o g i n ' => : c re a te
► d e le te 'lo g o u t' => rd estro y
► end

re so u rce s : users
reso u rces :o rd e rs
re so u rce s :lin e _ ite m s
re so u rce s : c a rts

get "s to re / in d e x "


reso u rces : products do
get :who_bought, on: :member
end
# ...
# You can have th e ro o t o f your s i t e routed w ith "r o o t"
# j u s t remember to d e le te p u b lic / in d e x .h tm l.
# ro o t :t o => 'welcom e#index'
ro o t t o : ' s t o r e # in d e x ', as: 's t o r e '
# ...
end

Мы уже сталкивались с этим, когда добавляли инструкцию ro o t в разделе 8.1,


«Шаг В1: создание каталога товаров». Команда генерации добавит к этому файлу
только универсальные инструкции g e t для каждого указанного действия. Вы мо­
жете (и должны) удалить маршруты, предоставляемые для sessions/new, sessions/
create и sessions/destroy.
В случае использования admin мы сократим URL-адрес, который пользователю
нужно ввести (удалив часть /index) и отобразим его на полное действие. В случае
действий, связанных с сессией, мы полностью изменим URL-адрес (заменив адреса
вида session/create простым адресом login), а также перенастроим соответствующее
HTTP-действие. Заметьте, что login отображается на оба действия, new и c re a te ,
различающиеся запросом: HTTP GET или HTTP POST.
Мы также воспользуемся кратчайшим путем: поместим объявления маршрутов
сессии в блок и передадим его методу класса c o n t r o l l e r ^ ). Это сократит коли­
чество набираемых символов и упростит чтение маршрутов. Все, что вы можете
сделать в этом файле, будет рассмотрено в разделе 20.1 «Диспетчеризация запросов
к контроллерам».
Располагая этими маршрутами, мы можем испытать радость входа в область
администрирования на правах администратора и увидеть окно, показанное на
рис. 14.3.
Глава 14 • Задача И: вход в административную область 219

Рис. 14.3. Интерфейс администрирования

Нам нужно заменить функциональные тесты в контроллере сессии, чтобы при­


вести все в соответствие с только что реализованными функциями:
ra ils3 1 / d e p o t_ r/ te s t/ fu n c tio n a l/ s e s s io n s _ c o n tro lle r_ te s t. rb
re q u ire ' t e s t _ h e lp e r '

c la s s S e s s io n s C o n tro lle rT e s t < A c tio n C o n tro lle r ::T e s tC a s e


t e s t "should get new" do
get :new
as se rt_re sp o n s e : success
end

► t e s t "should lo g in " do
► dave = u s e r s (:o n e )
► post :c r e a t e , name: dave.name, password: 's e c r e t '
► a s s e r t _ r e d ir e c te d _ to adm in_url
► a s s e rt_ e q u a l d a v e .id , s e s s io n [ : u s e r _ id ]
► end

► t e s t "should f a i l lo g in " do
► dave = u s e r s (:o n e )
► post :c r e a te , name: dave.name, password: 'wrong'
► a s s e r t _ r e d ir e c te d _ to lo g in _ u r l
► end

► t e s t "should lo g o u t" do
► d e le te : d e stro y
► a s s e r t _ r e d ir e c te d _ to s t o r e _ u r l
► end

end

А также нам нужно соответственно обновить тестовые стенды:


ra ils31/depot_r/test/fixtures/u sers.ym l
# Read about fixtures a t h t t p :/ / a p i.r u b y o n r a ils .o r g / c la s s e s / F ix t u r e s . html
o n e:
220 Часть И • Создание приложения

name: dave
p assw o rd_d igest: <%= B C ry p t: : Passw ord. c r e a t e ( ' s e c r e t ' ) %>

two:
name: M yStrin g
p assw o rd _d ig est: M yStrin g

Обратите внимание на использование в стенде динамически вычисляемых зна­


чений, в частности, для значения passw ord_digest. Чтобы добиться соответствия,
мы воспользовались той же функцией, которую для вычисления пароля использует
сама среда Rails1.
Мы показали последние результаты заказчику, и он заметил, что мы до сих пор
не контролируем доступ к страницам области администрирования (что в конечном
счете и было целью всех этих усилий).

14.3. Шаг ИЗ: ограничение доступа


Нам нужно отказать в доступе на страницы области администрирования всем, кто
не зарегистрировался в качестве администратора. Оказывается, упростить реали­
зацию этого замысла нам помогут имеющиеся в Rails фильтры.
Ф ильтры Rails позволяю т перехватывать вызовы методов действий, до­
бавляя свою собственную обработку перед тем, как методы будут реально
вызваны, после окончания их работы или и в том, и другом случае. Сейчас мы
воспользуемся фильтром до вызова, чтобы перехватить все вызовы действий,
находящиеся в нашем контроллере admin. Перехватчик может проверять значе­
ние s e s s io n [: u s e r _ id ] . Если оно установлено и соответствует пользователю,
зарегистрированному в базе данных, приложение знает, что в систему вошел
администратор и вызов можно продолжить. Если значение не установлено,
перехватчик может осуществить переадресацию, в нашем случае на страницу
авторизации.
А куда же нам следует поместить этот метод? Он может находиться непосред­
ственно в контроллере admin, но в силу причин, очевидность которых скоро станет
понятна, мы поместим его вместо этого в A p p lic a tio n C o n tro lle r —родительский
класс всех наших контроллеров. Он находится в файле application_controller.rb ката­
лога app/controllers. К тому же обратите внимание на то, что нам нужно ограничить
доступ к этому методу, чтобы он не был виден в качестве действия конечным
пользователям.

ra ils3 1 / d e p o t_ r/ a p p / c o n tro lle rs/ a p p lic a tio n _ c o n tro lle r. rb


c la s s A p p lic a tio n C o n t r o lle r < A c t io n C o n tr o lle r ::B a s e
► b e f o r e _ filt e r : a u th o riz e
# ...

► p ro te cte d

1 h ttp s://g ith u b .co m /ra ils/ra ils/b lo b /3 -l-stab le /active m o d e l/lib /a ctiv e_ m o d el/se cu re _ p ass-w o rd .rb
Глава 14 • Задача И: вход в административную область 221

► d e f a u th o riz e
► u n less U s e r .fin d _ b y _ id (s e s s io n [: u s e r _ id ] )
► r e d ir e c t _ t o lo g in _ u r l, n o tic e : "Пожалуйста, пройдите авторизацию”
► end
► end
end

Строка b e f o r e _ f i lt e r ( ) заставляет вызывать метод a u t h o r i z a t i o n ) перед


каждым действием в нашем приложении.
Это уже перебор. Нам нужно всего лишь ограничить доступ к области админи­
стрирования магазина, поэтому такое положение вещей нас не устраивает.
Мы можем вернуться и пометить только те методы, которые конкретно нужда­
ются в авторизации. Такой подход называется занесением в черный список, но при
его использовании можно допустить ошибку и что-нибудь пропустить. Намного
лучше применить подход «белого списка», то есть перечисления методов или кон­
троллеров, для которых авторизация не требуется. Мы сделаем это путем простой
вставки метода s k ip _ b e f o r e _ f ilte r ( ) в S to re C o n tro lle r:

ra ils B l/ d e p o t_ r/ a p p / c o n tro lle rs / s to re _ c o n tro lle r. rb


c la s s S t o r e C o n tr o lle r < A p p lic a tio n C o n t r o lle r
► s k ip _ b e fo r e _ filte r a u t h o r iz e

To же самое мы сделаем и для класса S e s s io n s C o n tro lle r:

ra ils3 1 / d e p o t_ r/ a p p / co n tro lle rs/ se ss io n s _ c o n tro lle r.rb


c la s s S e s s io n s C o n tro lle r < A p p lic a tio n C o n t r o lle r
► s k ip _ b e fo r e _ filte r : a u th o riz e

Но это еще не все. Теперь нам нужно разрешить создавать, обновлять и удалять
корзины:

ra ils3 1 / d e p o t_ r/ a p p / c o n tro lle rs/ c a rts_ c o n tro lle r. rb


c la s s C a r t s C o n tr o lle r < A p p lic a tio n C o n t r o lle r
► s k ip _ b e fo r e _ filte r a u t h o r iz e , o n ly : [ : c r e a t e , :update, :d e s tro y ]

создавать товарные позиции:


ra ils3 1 / d e p o t_ r/ a p p / co n tro lle rs/ lin e _ ite m s_ c o n tro lle r. rb
c la s s L in e lte m s C o n tro lle r < A p p lic a tio n C o n t r o lle r
► s k ip _ b e fo r e _ filte r '.a u th o riz e , o n ly : : c re a te

и создавать заказы (включая доступ к new):


ra ils3 1 / d e p o t_ r/ a p p / co n tro lle rs/ o rd e rs_ c o n tro lle r. rb
c la s s O rd e rs C o n tro lle r < A p p lic a tio n C o n t r o lle r
► s k ip _ b e fo r e _ filte r :a u th o riz e , o n ly : [:n ew , :c r e a t e ]
222 Часть И • Создание приложения

Располагая логикой авторизации, теперь мы можем перейти по адресу http://


localhost:3000/products. Метод фильтра осуществит перехват на пути к перечню то­
варов и покажет вместо него экран входа в область администрирования.
К сожалению, внесенное изменение сильно испортило большинство наших
функциональных тестов, поскольку большинство операций вместо выполнения же­
лаемой функции теперь будет перенаправляться на экран входа в административ­
ную область. Но, к счастью, мы можем воспользоваться глобальным обращением,
создав в test_helper метод setu p (). И раз уж мы оказались в этом файле, определим
заодно и несколько вспомогательных методов, позволяющих пользователю войти
в административную область под определенным именем — l o g in _ a s ( ) и выйти
из нее — lo g o u t().

ra ils3 1 / d e p o t_ r/ te st/ te st_ h e lp e r.rb


c la s s A c tiv e S u p p o rt::T e s tC a s e
# ...
# Add more h e lp e r methods to be used by a l l t e s t s h e r e .. .
d ef lo g in _ a s (u s e r )
s e s s io n [:u s e r _ id ] = u s e r s (u s e r ) . id
end

d ef logout
s e s s io n . d e le te :u s e r_ id
end

d ef setup
lo g in _ a s :one i f defined? session
end

Обратите внимание на то, что метод s e tu p Q будет вызывать l o g in _ a s ( ) ,


только если определена сессия. Тем самым авторизация не будет исполняться
в тестах, которые не задействуют контроллер. Мы показали результаты заказчику
и были награждены его широкой улыбкой и новым запросом: нельзя ли добавить
боковую панель, поместив на нее ссылки на функции администрирования поль­
зователей и товаров? И раз уж мы этим займемся, нельзя ли добавить возмож­
ность просмотра перечня и удаления пользователей с правами администратора?
Конечно, можно!

14.4. Шаг И4: добавление боковой


панели и дополнительных
административных функций
Начнем с добавления в макет ссылок на различные функции администрирования
на боковую панель и обеспечения их вывода только в том случае, если в сессии есть
сведения об идентификаторе пользователя — : usen_id:
Глава 14 • Задача И: вход в административную область 223

rails31/d ep ot_ r/ap p /view s/layo uts/ap p licatio n.h tm l.erb


<!DOCTYPE html>
<html>
<head>
< title > P rag p ro g Books O n lin e S to re < / title >
<%= s t y le s h e e t _ lin k _ t a g " a p p lic a t io n " %>
<%= ja v a s c r ip t _ in c lu d e _ t a g " a p p lic a t io n " %>
<%= c srf_ m e ta _ta g %>
</head>
<body class="<%= c o n tr o lle r .c o n t r o lle r _ n a m e %>">
<div id="banner">
<%= im a g e _ ta g ("lo g o .p n g ") %>
<%-- @ p a g e _ title | | "P rag m a tic B o o k s h e lf" %>
</div>
<div id="columns">
<div id=” side">
<% i f (Scart %>
<%= h id d e n _ d iv _ if(@ c a r t .lin e _ it e m s .e m p ty ? , id : ' c a r t ' ) do %>
<%= render g c a r t %>
<% end %>
<% end %>
<ul>
< l i x a h re f= "h ttp :// w w w .. . . "> H om e< /ax /li>
< l i x a h re f= "http://w w w . . . ./ fa q "> Q u e s tio n s < / a x / li>
< l i x a h re f= "http://w w w . .. ./news"> N ew s< /ax/li>
< l i x a h re f= "http://w w w . . . ./ c o n ta c t"> C o n ta c t< / a x / li>
</ul>

<% i f s e s s io n [ :u s e r _ id ] %>
<ul>
< lix % = lin k _ t o 'O r d e r s ', o rd ers_p ath % x / li >
< lix % = lin k _ t o 'P r o d u c t s ', prod ucts_path % x / li >
< lix % = li n k to 'U s e r s ’ , users_p ath % x / l.i>
</ul>
<%= b u tto n _to 'L o g o u t', lo g o u t p a t h , method: : d e le te %>
<% end %>
</div>
<div id="main">
<%= y i e ld %>
</div>
</div>
</body>
</html>

Теперь все начало собираться воедино. Мы можем войти в административную


область и, щелкнув на ссылке в боковой панели, просмотреть перечень пользова­
телей. Давайте посмотрим, не сломано ли что-нибудь при этом.

Давайте все-таки оставим


последнего администратора
Мы вызываем экран списка, который изображен на рис. 14.4, затем щелкаем
на ссылке Удалить, которая стоит рядом с именем dav e, чтобы удалить этого
224 Часть II • Создание приложения

пользователя. Будучи уверены, что наш пользователь удален, мы с удивлением


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

lo c a lh o s t

P r a g m a t ic B o o ksh elf

Questtonb
Listing users
New s Nam e
Contact
dave Show Edit Destroy

New U ser

Рис. 14.4. Вывод списка пользователей

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


командной строки. Если вызвать команду r a i l s co n so le, Rails вызовет утилиту
Ruby irb, но сделает это в контексте вашего Rails-приложения. А это означает, что
вы можете взаимодействовать с кодом своего приложения, набирая инструкции
Ruby и наблюдая за возвращаемыми значениями.
Мы можем воспользоваться этим обстоятельством для непосредственного
вызова нашей модели u s e r, заставив ее добавить для нас пользователя в базу
данных.
depot> r a i l s console
Loading development environm ent.
>> User.create(nam e: 'dave’ , password: ‘ s e c r e t ',
password_confirmation: ‘ s e c r e t ')
=> #<User:0x2933060 @ a t tr ib u te s = {. . . } ... >
>> User.count
=> 1
Последовательность символов > > представляет собой приглашение на ввод
данных. После появления первого приглашения мы вызываем класс User, чтобы
Глава 14 • Задача И: вход в административную область 225

создать нового пользователя, а после появления второго приглашения мы вызыва­


ем его еще раз, чтобы отобразить тот факт, что у нас действительно есть один поль­
зователь, занесенный в базу данных. После каждой введенной команды консоль
Rails выводит возвращенное кодом значение (в первом случае это объект модели,
а во втором — показания счетчика).
Паника закончилась, и мы можем опять пройти авторизацию и войти в админи­
стративную область. Но как предотвратить повторение подобной ситуации? Для
этого есть несколько способов. Например, мы можем написать код, предохраняю­
щий нас от удаления нашей собственной учетной записи. Но этот прием может не
сработать: теоретически некто А может удалить какого-то Б, а в то же самое время
некто Б может удалить этого А. Применим другой подход. Мы будем удалять поль­
зователя в процессе транзакции базы данных. Если после удаления пользователя
в базе данных никого не останется, мы отменим транзакцию, восстановив пользо­
вателя, который только что был удален.
Для этого мы воспользуемся методом отката, принадлежащим Active Record.
Один из таких методов мы уже рассматривали: откат v a l i d a t e вызывался Active
Record для проверки состояния объекта. Оказывается, Active Record определяет
около двадцати методов отката, каждый из которых вызывается в определенной
точке жизненного цикла объекта. Мы воспользуемся откатом a f t e r _ d e s t r o y ( ),
который вызывается после выполнения SQL-инструкции d e l e t e . Если метод
с таким именем является открытым и находится в области видимости, то он, что
для нас вполне подходит, будет вызван в той же самой транзакции, что и d e le t e ,
поэтому, если он выдает исключение, транзакция будет отменена. Метод отката
выглядит следующим образом:

rails31/depot_s/app/m odels/user. rb
a f te r _ d e s tr o y :ensure_an_adm in_rem ains

p r iv a t e
d e f ensure_an_adm in_rem ains
i f U s e r.c o u n t.z e ro ?
r a is e "Последний пользователь не может быть удален"
end
end

Для нас ключевым положением является использование исключения для инди­


кации ошибки при удалении пользователя. Это исключение преследует две цели.
Во-первых, поскольку оно выдается внутри транзакции, то становится причиной
автоматического отката. Выдавая исключение в том случае, если таблица u s e r s
после удаления опустошается, мы отменяем операцию удаления и возвращаем на
место запись, относящуюся к этому последнему пользователю.
Во-вторых, исключение сигнализирует контроллеру об ошибке там, где мы
используем блок b e g in - e n d для его обработки, и выводит пользователю флэш-
сообщение об ошибке. Если нужно только отменить транзакцию, но не сигнали­
зировать о возникновении исключения, следует вместо этого выдать исключение
A c t iv e R e c o r d :: R o llb a c k , поскольку это единственное исключение, которое не
будет передано кодом A c t iv e R e c o r d : : B ase . t r a n s a c t io n .
226 Часть II • Создание приложения

ra ils3 1 / d e p o t_ s/ a p p / co n tro lle rs/ u se rs_ co n tro lle r.rb


d e f d e stro y
(gluser = U ser.fin d (p a ra m s [: i d ] )
► begin
► (Sluser. d estro y
► flash[ :n o t ic e ] = "Пользователь #{|8user. name} удален"
► rescue Ex cep tion => e
► flash [ : n o tic e ] = e.message
► end

respond_to do |form at|


fo rm a t.h tm l { r e d ir e c t _ t o u s e r s _ u r l }
fo rm a t.js o n { head :ok }
end

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


исключено, что два администратора будут синхронно удалять двух последних
пользователей. Но для устранения этой проблемы понадобятся такие тонкости при
работе с базой данных, для которых в данной книге места не нашлось.
На самом деле рассмотренная в данной главе система входа в административ­
ную область довольно примитивна. В большинстве современных приложений
для этого используется специальный дополнительный модуль. Существует сразу
несколько дополнительных модулей, предоставляющих уже готовые решения, ко­
торые не только мощнее той логики аутентификации, которая была здесь показана,
но, в целом, требуют меньшего объема кода и усилий, предпринимаемых с вашей
стороны. Несколько примеров будут показаны в разделе 26.3 «Поиск новых до­
полнений в RailsPlugins.org».

14.5. Наши достижения


К окончанию данного шага нам удалось сделать следующее:
0 Воспользоваться методом h as_ secu re_ p assw o rd для хранения закодиро­
ванной версии пароля в базе данных.
0 Установить контролируемый доступ к функциям администрирования с ис­
пользованием фильтров b e fo re для вызова метода a u t h o r i z e ( ).
0 Посмотреть, как используется команда r a i l s co n so le для непосредствен­
ного взаимодействия с моделью (и для вытаскивания нас из ямы, в которую
мы провалились, удалив последнего пользователя).
0 Посмотреть, как транзакция может помочь в предотвращении удаления по­
следнего пользователя.

Чем заняться на досуге


Попробуйте проделать все это без посторонней помощи:
Глава 14 • Задача И: вход в административную область 227

О Модифицируйте функцию обновления данных о пользователе, чтобы она


запрашивала и проверяла текущий пароль, прежде чем позволить пользо­
вателю сменить этот пароль.
О Когда система только что установлена на новой машине, в базе данных нет
никаких сведений об администраторах, следовательно, ни один администра­
тор не может войти в административную область. Но, если на это не способен
ни один администратор, следовательно, никто не может создать пользовате­
ля с правами администратора. Внесите в код изменения, позволяющие при
отсутствии в базе данных сведений об администраторах входить в админи­
стративную область под любым именем пользователя (что позволит быстро
создать настоящего пользователя с правами администратора).
О Поэкспериментируйте с командой r a i l s c o n s o le . Попробуйте создать
товары, заказы и товарные позиции. Проследите за возвращаемым значе­
нием при сохранении объекта модели — когда проверка допустимости не
будет пройдена, вы увидите, что будет возвращено значение f a l s e returned.
Определите причины следующих ошибок:
>> prd = Product.new
=> #<Product id : n i l , t i t l e : n i l , d e s c r ip tio n : n i l , im ag e_u rl:
n i l , c re a te d _ a t: n i l , upd ated _at: n i l , p r ic e :
#< BigD ecim al: 2 4 6 a a lc ,'0 .0 ',4 (8 )> >
>> prd.save
=> f a ls e
>> p rd.errors.full_m essages
=> ["Im age u r l must be a URL f o r a G IF , JPG , or PNG im age",
"Image u r l c a n 't be b la n k ", " P r ic e should be at le a s t 0 .0 1 ",
" T i t l e c a n 't be b la n k ", "D e s c r ip tio n c a n 't be b la n k "]

О Посмотрите на метод a u th e n t ic a te _ o r _ r e q u e s t_ w it h _ h tt p _ b a s ic ()
и примените его в своем фильтре : a u th o r iz e , если r e q u e s t . fo rm a t не
относится к Mime: :HTML. Проверьте его работоспособность путем доступа
к RSS-каналу Atom:
c u r l - - s ile n t --user d a v e :s e c r e t \
h t t p :/ / lo c a lh o s t : 3000/products/2/who_bought.xml

О Добившись работоспособности наших тестов при входе в административную


область, мы еще не написали тесты, проверяющие, что доступ к конфиден­
циальным данным требует входа в эту область. Напишите хотя бы один тест,
проверяющий это путем вызова метода lo g o u t () с последующей попыткой
извлечь и обновить некоторые данные, требующие для этого аутентифика­
ции пользователя.
(П одсказки можно найти по адресу http://www.pragprog.com/wikis/wiki/
RailsPlayTime.)
Задача К:
локализация

О сн о в н ы е тем ы :

> ш аблоны локализации;


> разбор конструкции базы д ан н ы х для И 8 п .

Теперь у нас есть работоспособная основная корзина покупателя, и наш заказчик


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

ДЖ О СПРАШ ИВАЕТ: НУЖ НО ЛИ ЧИТАТЬ ЭТУ ГЛАВУ, ЕСЛИ Я СОБИРАЮ СЬ


ИСПОЛЬЗОВАТЬ ТОЛЬКО ОДИН Я З Ы К ? -----------------------------------------------------
Если коротко, то нет. Ф актически, многие Rails-прилож ения предназначены для неболь­
ш их или однородны х групп и никогда не нуж даю тся в переводе. Но следует сказать, что
практически каждый, обнаруж ивш ий н еобходимость в переводе, соглаш ается с тем, что
было бы лучш е, если бы такая возмож ность была залож ена как мож но раньше. Поэтому,
если вы не уверены в том, что перевод никогда не понадобится, мы рекомендуем вам хотя
бы получить представление, о том, что за этим стоит, чтобы вы могли принять взвеш ен­
ное решение.
Глава 15 • Задача К: локализация 229

Уже проще, но задача все равно нелегкая. Нам надо предоставить способ, по­
зволяющий пользователю выбрать язык, предоставить сам перевод, а также внести
изменения и в представления, чтобы воспользоваться этим переводом. Мы готовы
к выполнению задачи, вооружены скромными познаниями в испанском на уровне
средней школы — пора действовать!

15.1. Шаг К1: выбор региона


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

ra ils 3 1 / d e p o t_ s / c o n fig / in itia liz e rs / il8 n . rb


«encoding: u tf-8
I1 8 n .d e f a u lt _ lo c a le = :en

LANGUAGES = [
[ 'E n g lis h ', 'e n '] ,
[ " E s p a & n t ild e ;o l" . h tm l_s a fe , ' e s ']
]
Этот код выполняет две задачи.
Прежде всего, он приводит к использованию модуля 118п для установки ре­
гиона по умолчанию. 118п выглядит забавно, но зато обеспечивает неизменно пра­
вильный набор термина, эквивалентного слову internationalization (локализация).
Ведь слово «internationalization» начинается на «i» и заканчивается на «п», и имеет
восемнадцать букв между ними.
Затем определяется список связей между отображаемыми и локальными име­
нами. К сожалению, на данный момент мы располагаем только лишь клавиатурой
с раскладкой, предназначенной для США, а в слове espanol имеется символ, ко­
торый невозможно ввести с нашей клавиатуры напрямую. Разные операционные
системы обладают разными способами решения этой проблемы, и зачастую про­
ще всего скопировать и вставить правильный текст с веб-сайта. При этом нужно
убедиться в том, что ваш редактор настроен для работы с UTF-8. Тем временем
мы решили использовать HTML-эквивалент испанского символа «п с тильдой».
Если не сделать ничего другого, будет показана сама разметка. Но за счет вызова
метода h tm l_ sa f е мы информируем Rails о том, что строка может быть безопасно
интерпретирована как содержащая HTML.
Чтобы Rails восприняла эти изменения конфигурации, нужно перезапустить
сервер.
Поскольку каждая страница, подвергаемая переводу, будет иметь еп- и e s -
версию (на данный момент многое еще предстоит добавить), есть смысл включить
это в URL-адрес. Давайте спланируем поместить указание на регион авансом, сде­
лать его необязательным и по умолчанию использовать текущий регион, который,
в свою очередь, будет по умолчанию настроен на английский.
230 Часть II • Создание приложения

Для реализации этого хитрого плана давайте начнем с изменения файла config/
routes, rb:

rails31/depot_s/config/ ro u te s. rb
D e p o t:A p p lic a t io n .r o u te s .d r a w do
get 'adm in' => ' adm in#index'
c o n t r o lle r : sessio n s do
get 'l o g i n ' => :new
post 'l o g in ' => : c re a te
d e le te 'lo g o u t' => : d estro y
end
► scope ' ( : l o c a l e ) ' do
re so u rces : users
re so u rces : orders
re so u rces :lin e _ ite m s
re so u rces : c a r t s
re so u rces : products do
get :who_bought, on: : member
end
ro o t t o : ' s to re # in d e x ' , a s : 's t o r e '
► end
end

Здесь мы вложили наши объявления ресурсов и исходных точек входа в объ­


явление области видимости для : lo c a le . Кроме того, обозначение : l o c a le взято
в круглые скобки, что является способом сообщить о его необязательном характе­
ре. Обратите внимание на то, что мы не стали помещать в эту область видимости
административные функции и функции сессии, поскольку пока не собираемся их
переводить.
Это значит, что при наборе адреса http ://localhost: 3000/ будет использован регион
по умолчанию (English), и поэтому будет указан точно такой же маршрут, как и при
указании адреса http://localhost:3000/en. При наборе адреса http://localhost:3000/es
маршрут приведет к тому же контроллеру и действию, но нам нужно будет, чтобы
это вызвало другую установку локализации.
Для этого нужно создать фильтр b e f o r e _ f ilte r и установить d e f a u l t_ u r l_
o p tio n s . Логика для выполнения всего этого помещается в общий базовый класс
для всех наших контроллеров, то есть в A p p lic a tio n C o n tro lle r:

ra ils3 1 / d e p o t_ s/ a p p / c o n tro lle rs/ a p p lic a tio n _ c o n tro lle r.rb


c la s s A p p lic a tio n C o n t r o lle r < A c t io n C o n t r o lle r : : Base
► b e f o r e _ filt e r : s e t_il8 n _lo ca le _fro m _p a ra m s
# ...
p ro te cte d

► d e f s e t_il8 n _lo ca le _fro m _p a ra m s


► if param s[: lo c a le ]
► i f I1 8 n . a v a i la b l e _ lo c a l e s . in c lu d e ? (p a ra m s [: lo c a le ],to _ s y m )
► I1 8 n .lo c a le = param s[: lo c a le ]
► e ls e
► flash . now [: n o tic e ] =
► "# {p aram s[ : l o c a l e ] } t r a n s la t io n not a v a ila b le "
Глава 15 • Задача К: локализация 231

# перевод недоступен
lo g g e r .e r r o r fla s h .n o w [:n o tic e ]
end
end
end

d e f d e f a u lt _ u r l_ o p t io n s
{ lo c a le : I1 8 n .lo c a le }
end
end

Этот метод s e t_ il8 n _ lo c a le _ f rom_params делает практически все в соответ­


ствии со своим названием: устанавливает локальные настройки из параметров, но
только в том случае, если они там есть, в противном случае он оставляет текущие
локальные настройки без изменений. Позаботился он и об оповещении в случае не­
возможности установки локальных настроек как пользователя, так и администратора.
Метод d e f a u lt_ u r l_ o p tio n s также делает практически то, что указано в его
имени, —предоставляет хэш из URL-дополнений, которые считаются предоставлен­
ными, если они на самом деле не предоставлены. В данном случае мы предоставляем
значение для параметра : lo cale. Это необходимо, когда представление, находящееся
на странице, не имеющей указаний на регион, пытается составить ссылку на страни­
цу, имеющую такие указания. Как это используется, мы скоро увидим.
При наличии всего этого мы получим результат, показанный на рис. 15.1.

(2 ? й ' jQ lo c a lh o s t 3000/еп & ^


:
P r a g m a t ic B o o ksh elf
-
Home
Your Pragmatic Catalog
Q uestions
News щгзж CoffeeScript
Contact c.*-csrtip«_ CoffeeScript is Ja va Scrip t clone right ft provides ail of
Ja va S crip t’s functionality wrapped in a cleaner, more
fc r— su ccin ct syntax. In trie first book on this exciting new
language. CoffeeScript guru Trevor Burnham show s you
how to hold onto all the power and flexibility of
Ja va S crip t while writing clearer, cleaner, and safer code.

$36.00 Add to Cart :

Рис. 15.1. А н г л и й с к а я в е р с и я п е р в о й с т р а н и ц ы

На данный момент английская версия страницы доступна при обращении как


к главной странице веб-сайта, так и к странице, начинающейся с /еп. Кроме этого,
у нас еще есть код, выводящий на экран сообщение о том, что перевод недоступен
(показанное на рис. 15.2), а также оставляющий в регистрационном журнале со­
общение, свидетельствующее о том, что файл не был найден. Оно не передает
именно этот смысл, но все равно приносит пользу.
232 Часть II • Создание приложения

foTocathost зооо/es________________________________ tf Л

P r a g m a t ic B o o k s h e l f

es translation not available

Your Pragmatic Catalog

C offe e S crip t
C offeeScrip t is J a va S c rip t done right It provides all of
Ja v a S c rip t’s functionality w rapped in a cleaner, more
su c c in c t syntax. In the first book on this exciting new
language, CoffeeScrip t guru Trevor Burnham show s
y o u how to hold onto all the power and flexibility of
J a va S crip t while writing clearer, cleaner, and safer
code.

$ 3 6 .0 0 A d d to Cart

Рис. 15.2. Перевод недоступен

15.2. Шаг К2: перевод каталога товаров


Настало время предоставить переведенный текст. Начнем с макета, как с самого
наглядного кода. Заменим любой тест, нуждающийся в переводе, вызовом 118п.
t r a n s l a t e . У этого метода не только есть удобный псевдоним I 1 8 n .t, но для него
также предоставлен помощник по имени t.
Аргумент для функции перевода является уникальным именем, обозначен­
ным точкой. Можно выбрать любое понравившееся имя, но, если используется
предоставленная вспомогательная функция t , имена, начинающиеся с точки,
должны быть сначала расширены с использованием имени шаблона. Давайте так
и сделаем.

rails3 1/d ep ot_ s/ap p /view s/layouts/ap plication. htm l. erb


<!DOCTYPE html>
<html>
<head>
< title > P rag p ro g Books O n lin e S to re < / title >
<%= s t y le s h e e t _ lin k _ t a g " a p p lic a t io n " %>
<%= ja v a s c r ip t _ in c lu d e _ t a g " a p p lic a t io n " %>
<%= c srf_ m e ta _ta g %>
</head>
cbody class="<%= c o n t r o l l e r . co n tro lle r_n a m e %>">
<div id="banner">
<%= im a g e _ ta g ("lo g o .p n g ") %>
Глава 15 • Задача К: локализация 233

<%= @ p a g e _ t i t l e || t ( ' . t i t l e ' ) %>


</div>
< d iv id = "c o lu m n s">
< d iv i d = " s i d e " >
<% i f @ c a r t %>
<%= h i d d e n _ d i v _ i f ( @ c a r t . l i n e _ i t e m s . e m p t y ? , id : 'c a r t ') do %>
<%= r e n d e r (Slcart %>
<% end %>
<% end %>

<ul>
< l i x a h r e f = " h t t p : / / w w w . . . ."><%= t ( ' . h o m e ' ) % > < / a x / l i >
< l i x a h r e f = " h t t p : / / w w w . . . ,/faq"><%= t ( ' . q u e s t i o n s ' ) % > < / a x / l i >
< l i x a h r e f = " h t t p : / / w w w . . . ./ n e w s "x % = t ( ' . n e w s ' ) % x / a x / l i >
< l i x a h re f= "http://w w w . .. ,/ c o n ta c t" x % = t ( ' . c o n t a c t ') % x / a x / l i >
</ul >
<% i f s e s s i o n [ : u s e r _ i d ] %>
<ul>
< lix % = l i n k _ t o ' O r d e r s ' л o rd e rs_ p a th % x / l i >
< lix % = l i n k _ t o ' P r o d u c t s ' л pro d u cts_ p ath % x / l i >
< lix % = l i n k _ t o ' U s e r s ' , u sers_ path % x / l i >
</ul>
<%= b u t t o n _ t o ' L o g o u t ' , l o g o u t _ p a t h , method: :d e l e t e %>
<% end %>
</div>
< d iv id = "m ain ">
<%= y i e l d %>
</div>
</div>
</body>
</html>

Поскольку это представление называется layouts/application.html.erb, английское


отображение будет расширено до en.layouts.application. А вот соответствующий файл
локализации:

rails31/depot_s/config/locales/en.ym l
en:

la y o u ts :
a p p lic a tio n :
title : "Prag m a tic B o o k s h e lf"
home: "Home"
q u e s tio n s : "Q u e stio n s "
news: "News"
co n tact: "Contact"

и следующий файл на испанском:

rails31/depot_s/config/locales/es.ym l
es:

la y o u ts :
234 Часть II • Создание приложения

a p p lic a t io n :
title : "P u b lic a c io n e s de P ra g m a tic ”
home: " In ic io "
q u e s tio n s : "P re g u n ta s"
news: " N o t ic ia s "
c o n ta c t: "C o n ta cto "

В них используется формат YAML —такой же формат использовался для кон­


фигурирования баз данных. YAML состоит из расположенных с отступами имен и
значений, где доступ в данном случае соответствует структуре, создаваемой в на­
ших именах.
Чтобы заставить Rails распознать наличие новых YAML-файлов, нужно пере­
запустить сервер.
На рис. 15.3 мы можем увидеть реально переведенный текст, появляющийся
в окне нашего браузера.

Р UВ!,1CАС ЮМЕS DF PRAGMATIC


с
IntCK)
Your Pragmatic Catalog
......................... ........................... ..................... . ............................. I
PfBtnmias ■ \
twicias ■ ■ : | C offe e S crip t
CoiHMwt ,1. ■ ш к.,... C offeeScript is Ja v a S c rip t done right. H p sw ities all of 1
^ .J
Ja v a S c rip t’s functionality wrapped in a cleaner, more |
'' Л ' ‘ ^ ftp su c c in c t syntax, In the first book on this exciting new
Щ г -
language. C o ffeeScrip t guru Trevor Burnham shows
yo u how to hold onto all rhe power ar>d flexibility of
J a va S c rip t w hile wTiting clearer, cleaner, and safer
code.

$ 3 6 .0 0 A d d to Cart

Рис. 15.3. Робкие шаги: перевод заголовков и боковой панели

Следующим нз^жно обновить основной заголовок, а также кнопку Add to


Cart (Добавить в корзину). И то и другое можно найти в шаблоне каталога мага­
зина:

rails31/depot_s/app/view s/store/lndex. htm l. erb


<% i f n o tic e %>
<p id="notice"><%= n o tic e %></p>
<% end %>

►<hl><%= t ( ' . t i t l e _ h t m l ' ) %></hl>

<% ^ p ro d u c ts. each do |product!


Глава 15 • Задача К: локализация 235

<div c la s s = ” entry">
<%= im a g e _ta g (p ro d u ct.im a g e _u rl) %>
<h3><%= p r o d u c t . t i t le %></h3>
<%= s a n it iz e (p r o d u c t .d e s c r ip t io n ) %>
<div c la s s = "p r ic e _ lin e " >
<span class= "price"> < %= n u m b e r_to _c u rre n c y (p ro d u c t.p ric e ) %></span>
► <%= b u tto n _to t ( ' .a d d _ h tm l') , lin e _ it e m s _ p a t h (p r o d u c t_ id : p ro d u c t),
remote: tr u e %>
</div>
</div>
<% end %>

А вот как выглядят соответствующие обновления файлов локализации, сначала


на английском:
rai.ls31/depot_s/config/locales/en.ym l
en:

sto re :
index:
t i t l e _ h t m l: "Your Pragm atic C a ta lo g "
add_htm l: "Add to C a rt"

а затем на испанском:

rails31/depot_s/config/locales/es. yml
es:

s to re :
index:
title _ h tm l: "Su C at& aa cu te;lo g o de Prag m atic"
add_html: "A & n tild e ; a d ir a l C a r r it o "

Следует заметить, что, поскольку t i t l e _ h t m l и add_htm l заканчиваются сим­


волами _htm l, мы можем свободно использовать имена, существующие в HTML
для обозначения символов, не встречающихся на нашей клавиатуре.
Если вы не дали ключу перевода подобного имени, на странице будет видна
сама разметка. Это еще одно соглашение, принятое в Rails для облегчения вашей
программистской жизни. Rails будет также рассматривать имена, содержащие html
в качестве компонента (иными словами, содержащие строковое значение . h tm l.)
ключевых имен HTML.
Обновив страницу в окне браузера, мы увидим результаты, показанные на
рис. 15.4.
Обретя уверенность, возьмемся за парциал корзины:
rails31/depot_s/app/view s/carts/_cart. htm l.erb
►<div c la s s = "c a rt_ title "> < % = t ( ' . t i t l e ' ) %></div>
<table>
<%= r e n d e r (c a r t .lin e _ it e m s ) %>

< tr c la s s = " t o t a l lin e ">


236 Часть II • Создание приложения

<td colspan="2">Total</td>
<td c la s s = "to ta l_ c e ll"> < % = n u m b e r _ to _ c u r r e n c y (c a r t.to ta l_ p r ic e ) %></td>
</tr>
</table>

►<%= b u tto n _to t ( ' .c h e c k o u t ') , new _order_path, method: :g e t %>


►<%= b u tto n _to t ( '. e m p t y ') , c a r t , method: :d e le te ,
confirm: 'A re you s u r e ? ' %>

P u b l ic a c io m e s d e P r a g m a t ic
Su Catalogo de Pragmatic

C o ffe e Script
C offeeScript is J a va S c rip t done right. It provides all of
J a va S c rip t's functionality wrapped in a cleaner, more
su ccin ct syntax. In the first book on this exciting new
language. C o ffe e Scrip t guru Trevor Burnham show s
you how to hold onto ail the power and flexibility of
Ja va S crip t while writing clearer, cleaner, and safer
code

S36.00 A nad ir al Carrito

Рис. 15.4. Переведенные заголовок и кнопка

И опять переводы:

rails31/depot_s/config/locales/en.ym l
en:

ca rts:
ca rt:
title : "Your C a rt"
em pty: "Empty c a r t "
ch eck o u t: "Checkout"

rails31/depot_s/config/locales/es.ym l

ca rts:
ca rt:
title : "C a r r it o de la Compra"
empty: "V a c ia r C a r r it o "
ch eck o u t: "Comprar"
Глава 15 • Задача К: локализация 237

Обновив страницу, мы увидим, что заголовки корзины и кнопки были пере­


ведены:

localhost

P u b l ic a c io n e s d e P r a g m a t ic

Carrito de la Cranpra Su Cat&logo de Pragmatic


1* C offeeScript $36 00
Total $36 00 C o ffe e S c rip t
C offeeScrip t is Ja va S crip t done right. It provides all of
^ornprarjplj
Ja va S c rip t's functionality wrapped in a cleaner more
V a c ia r Carrito su c c in c t syntax. In the first book on this exciting new
language. C offeeScript guru Trevor Burnham show s
tnicio you how to hold onto all the power and flexibility of
Preguntas Ja v a S c rip t while writing clearer, cleaner, and safer
N oticias code.
C ontacto
S36.00 Anadir a! Carrito

И теперь мы заметили нашу первую проблему. Локализация затрагивает не


только язык, но и представление валюты. Изменяется также и общепринятый
способ представления чисел.
Поэтому мы сначала обмениваемся мнениями с заказчиком и убеждаемся в том,
что пока нам не нужно возиться с обменными курсами, поскольку этим займутся
компании, обслуживающие кредитные карты и (или) безналичную торговлю, но
при показе результата на испанском нам придется выводить после числового зна­
чения строку «USD» или «$US».
Еще одним изменением будет способ вывода самих чисел. Десятичные значения
отделяются запятой, а разделителем тысячных позиций служит точка.
С валютой все немного сложнее, чем кажется на первый взгляд, поэтому нужно
принять множество решений. К счастью, Rails в курсе, что для получения этой
информации нужно заглянуть в ваш файл перевода, нам нужно лишь все это предо­
ставить. Сначала для еп:

rails31/depot_s/config/locales/en.yml
en:

number:
c u rre n c y :
fo rm a t:
u n it :
p re c is io n
se p a ra to r
d e lim it e r
fo rm a t:
238 Часть II • Создание приложения

А затем для es:

rails31/depot_s/config/locales/es. yml

number:
cu rre n c y:
fo rm a t:
u n it : "$US"
p r e c is io n : 2
s e p a ra to r:
d e lim it e r :
fo rm at: "%n&nbsp;%u"

Мы указали денежную единицу ( u n i t ) , точность ( p r e c i s i o n ) , разделитель


десятичной части ( s e p a r a t o r ) и разделитель тысячных позиций ( d e l i m i t e r ) для
number, c u r r e n c y . f o r m a t . Все это вполне очевидно. А вот формат ( f o r m a t ) вы­
глядит немного сложнее: %п —это указатель места вставки самого числа, S n b s p ; —
это символ неразрываемого пробела, уберегающий это значение от разбиения на
несколько строк, а %и —э го указатель места вставки денежной единицы.

i_i Ргадргод Books Online :

& ш lo c a lh o s t:3 0 0 0 /e s "Ч

P u b l ic a c io n e s de Pragm a t ic

Carrito de la Compra Su Catalogo de Pragmatic


1* CoffeeScrip t 36.00 $ U S
Total 36.00 S U S C offe e S crip t
CoffeeScript is Ja v a S c rip t done right. It provides all of
Ja v a S c rip t's functionality wrapped in a cleaner, more
s u c c in c t syntax. In the first book o n this e xcitin g new
* r- language. C o ffeeScrip t guru Trevor Burnham show s
you how to hold onto all the power and flexibility of
Ja va S crip t w hile writing clearer, cleaner, and safer
code

36.00 SU S Anadir al Carrito

15.3. Шаг КЗ: перевод оформления заказа


Теперь мы на финишной прямой. Следующей будет страница заказа:
rails31/depot_s/app/views/orders/new. html. erb
<div class= "depot_form ">
<fieldset>
► <legend><%= t ( ' . l e g e n d ' ) %></legend>
Глава 15 • Задача К: локализация 239

<%= render 'form' %>


</fieldset>
</div>

Затем форма, используемая этой страницей:

rails31/depot_s/app/views/orders/_form . htm l. erb


<%= form_for((S)order) do |f| %>
<% i f (8order.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize((g)order .e r r o r s . count, "e r r o r " ) %>
prohibited th is order from being saved:</h2>

<ul>
<% b o rd e r.erro rs.fu ll_m essa g e s. each do |msg[ %>
<li><%= msg %></li>
<% end %>
</ ul>
</div>
<% end %>

<div class="field">
<%= f . la b e l :name %><br />
<%= f.te x t_ fie ld :name, siz e: 40 %>
</div>
<div class="field">
► <%= f . la b e l :address, t ( ' . address_html' ) %><br />
<%= f.te x t_ a re a :address, rows: 3, c o ls : 40 %>
</div>
<div class="field">
<%= f . la b e l :em ail %><br />
<%= f.em a il_fie ld : em ail, siz e : 40 %>
</div>
<div class="field">
<%= f . la b e l :pay_type %><br />
<%= f . s e le c t : pay_type, Order::PAYMENT_TYPES,
► prompt: t ( ' . pay_prompt_html' ) %>
</div>
<div class= "actions">
► <%= f.subm it t ( ‘ .su b m it') %>
</div>
<% end %>

Обратите внимание: нам, как правило, не нужно вызывать функции 118п в от­
ношении надписей в явном виде, пока не возникнет потребность в каких-нибудь
особенных действиях вроде разрешения HTML-объектов. А вот как выглядят со­
ответствующие определения локализации:

rails31/depot_s/config/locales/en. yml

o rd e rs:
new:
240 Часть II • Создание приложения

le g e n d : "P le a s e E n te r Your D e t a ils "


form:
name: "Name"
a d d ress_h tm l: "A ddress"
e m a il: "E - m a il"
p ay_type: "Pay w ith "
pay_prom pt_htm l: " S e le c t a payment method"
subm it: "P la c e O rder"

rails31/depot_s/config/locales/es.ym l

o rd e rs:
new:
le g e n d : "P o r fa v o r , in tro d u z ca sus d ato s"
form:
name: "Nombre"
add ress_htm l: "D ire c c i& o a c u te ;n "
e m a il: "E - m a il"
p a y _ ty p e : "Forma de pago"
pay_prom pt_htm l: "S e le c c io n e un m &eacute;todo de pago"
subm it: " R e a liz a r Pedido"

Готовая форма показана на рис. 15.5.

j_, Ргадргод Books Online S

'Ш [ Q to c a i h o s t 3 0 0 0 /e s/o fd e fs/n e w ? ☆I л,

P u b l ic a c io n e s d e P r a g m a t ic

Рис. 15.5. Выражение готовности получить ваши деньги на испанском

Все выглядело неплохо, пока мы преждевременно не щелкнули на кнопке


Realizar Pedido и не увидели уведомления, показанного на рис. 15.6. Сообщение об
Глава 15 • Задача К: локализация 241

ошибке, создаваемое Active Record, также может быть переведено, нам нужно будет
только предоставить перевод:

rails31/depot_s/config/locales/es. yml
es:

a c t iv e r e c o r d :
e rro rs:
messages:
i n c lu s io n : "no e st& a a c u te ; in c lu id o en la l i s t a "
b la n k : "no puede quedar en bian co"

e rro rs:
te m p late :
body: "Hay problemas con lo s s ig u ie n te s campos:"
he ad er:
one: "1 e r r o r ha impedido que e s te % {m odel} se guarde"
o th e r: "%{count> e rr o re s han impedido que e s te %{m odel} se guarde"

Обратите внимание на то, что сообщения с числами имеют, как правило, две
формы: e r r o r s . te m p la te . h e a d er. one —это сообщение, создаваемое при возникно­
вении одной ошибки, и e r r o r s . te m p la te . header .o th e r —сообщение, создаваемое
во всех остальных случаях. Это позволяет переводчикам предоставить правильную
множественную форму существительных и соответствие глаголов существительным.
ШЯшшщи щ и и и и и и и м м
Pragprog Books Online \;*ШЪ t „ Ctf

j ^'Й^'В[ В Н Н ВВннВВВВВВН
0 locelhost'3000/es/orders

P u b l ic a c io n e s d e P r a g m a t ic
Garrito de la Compra P o r favor. introdusca su s datos
1* CoffeeScrip t 36.00 $ U S
4 e rro rs prohibited this order from being saved:
Total 36,00 SU S

Name translation missing:


es.activerecord.errors.model5.order.attributes,name.blank
Address translation missing:
es.activerecord.errors.models.Gfder.attributes.address.blank
tree io Email translation missing:
Preoufitas esactiverecord.errors.modeIs.order.attributes.email.biank
News Pay type translation missing:
Com aeto es.activerecord.errors modeis.order.attributes,pay_type.inclusion

Рис. 15.6. Перевод отсутствует

Поскольку мы опять используем HTM L-объекты, нам нужно, чтобы эти со­
общения об ошибках выводились как есть (в понятиях Rails — в необработанном
виде). Нам также нужно перевести сообщения об ошибках. Модифицируем форму
еще раз:
242 Часть II • Создание приложения

r a il s 3 1 / d e p o t _ t / a p p / v ie w s / o r d e r s / _ f o r m . h t m l. e r b
<%= fo rm _fo r(@ o rd e r) do | f | %>
<% i f (Sorder. e r r o r s . any? %>
<div id = "e r r o r _ e x p la n a tio n ">
► <h2x%=raw t ( ' e r r o r s .te m p la te , head er' , count: @ o rd e r.e rro rs .c o u n t,
► model: t ( ' a c tiv e r e c o r d .m o d e ls .o rd e r' ) ) %>.</h2>
► <p><%= t ( 'e r r o r s .t e m p la t e .b o d y ’ ) %></p>

<ul>
<% (So rd e r.e rro rs ,fu ll__m e s s a g e s . each do |msg| %>
► < lix % = ra w msg % x / li >
<% end %>
</ul>
</div>
<% end %>
<!-- ... -->
Обратите внимание на то, что мы вызову метода для перевода заголовка ша­
блона ошибок передаем количество ошибок и имя модели (которая сама по себе
допускает возможность перевода).
После внесения этих изменений мы повторяем нашу попытку и видим улучше­
ния, показанные на рис. 15.7.

Рис. 15.7. Английские существительные в испанских предложениях

Уже лучше, но через интерфейс просачиваются имена модели и свойств. Это


вполне допустимо на английском, поскольку названия подбирались для работы на
этом языке. Нам нужно предоставить перевод для каждого имени.
Он также должен быть в YAML-файле:
rails31/depot_t/config/locales/es.ym l

a c t iv e r e c o r d :
m odels:
o rd e r: "ped id o"
a ttr ib u te s :
o rd e r:
Глава 15 • Задача К: локализация 243

add ress: "D ire c c i& o a c u te ;n "


name: "Nombre"
e m a il: "E - m a il"
p ay_typ e: "Forma de pago"

Обратите внимание на то, что нам не нужно предоставлять для всего этого ан­
глийские эквиваленты, потому что эти сообщения встроены в Rails.
Нам приятно видеть переведенные имена модели и свойств на рис. 15.8; мы за­
полнили форму, отправили заказ и получили сообщение «Thank you for your order»
(«Спасибо за ваш заказ»).

PUBLICACIQNES DE P r ACM ATI С


Gairrto de ia Coovpra P or favor introduzca su s datos
IX CoffeeScript 36,00 $ U S
4 errores han impednJo que este pedido se guard©.

Body Htm!

■ Nombre no puede quedar en bianco


• Direction no puede quedar en bianco
iracio - E-mail no puede quedar en bianco
Prequntas • Forma de pago no esta indtado en Sa lista

Рис. 15.8. Теперь переведены и имена модели

Нам нужно обновить флэш-сообщения:


ra ils3 1 / d e p o t_ t/ a p p / co n tro lle rs/ o rd e rs_ c o n tro lle r.rb
def c re a te
(Sorder = O rd e r. new(params[ : o r d e r ] )
(Sorder .a d d _ lin e _ ite m s _ fr o m _ c a r t (c u r r e n t _ c a r t)
respond_to do |form at|
i f (Sorder.save
C a r t . d e s tro y ( s e s s io n [ : c a r t _ i d ] )
s e s s io n [ : c a r t _ id ] = n i l
O rd erN o tifier. re c e iv e d ((Sorder) . d e l iv e r
fo rm a t.h tm l { r e d ir e c t _ t o s t o r e _ u r l, n o tic e :
► I 1 8 n . t ( ' .t h a n k s ') }
fo rm a t.js o n { render js o n : (Sorder, s t a tu s : :c re a te d ,
lo c a t io n : (Sorder }
e ls e
@ cart = c u r r e n t_ c a r t
fo rm a t.h tm l { render a c tio n : "new" }
fo rm a t.js o n { render js o n : (S o rd e r.e rro rs ,
s t a tu s : : u n p ro c e s s a b le _ e n tity }
end
end
end
244 Часть II • Создание приложения

И наконец, предоставим переводы:

rails31/depot_t/config/locales/en.ym l
en:
th anks: "Thank you f o r your o rd e r"

rails31/depot_t/config/locales/es.ym l
es:

th anks: "G r a c ia s por su pedido"

Благодарственное сообщение показано на рис. 15.9.


.. -х

☆ Л,

P u b l ic a c io n e s de P r a g m a t ic

Gracias рог su pedido

Su Catalogo de Pragmatic

C offe e Script
C offeeScript is Ja va S crip t done right. It provides all of
Ja va S c rip t's functionality wrapped in a cleaner more
s u c c in c t syntax. In the first book on this exciting new
language. C o ffeeScrip t guru Trevor Burnham show s you
how to hold onto all the power and flexibility of
Ja va S crip t w hile writing clearer, cleaner and safer code.

36,00 SUS Anadir al Carrito

Рис. 15.9. Благодарим пользователя на испанском

15.4. Шаг К4: добавление переключателя


локализации
Мы завершили выполнение задачи, но теперь нам нужно дополнительно обозна­
чить ее возможности. Мы присмотрели одну неиспользуемую область в верхней
правой части макета, поэтому дополним форму непосредственно перед image_
tag:
Глава 15 • Задача К: локализация 245

rails31/d ep ot_ t/ap p /view s/layo uts/ap p licatio n. htm l. erb


<div id="banner">
► <%= form _tag s to re _ p a th , c la s s : 'l o c a l e ' do %>
► <%- s e l e c t t a g ' s e t _ lo c a l e ' ,
► o p tio n s_fo r_select(LA N G U A G ES, I1 8 n . lo c a l e . t o _ s ) ,
► onchange: 't h i s . f o r m . s u b m i t ( ) ' %>
► <%= subm it_tag ’ subm it' %>
► <%= ja v a s c r ip t _ t a g " $ ( ' . l o c a le in p u t ' ) . h id e ( ) " %>
► <% end %>
<%= im a g e _ ta g ("lo g o .p n g ") %>
<%= @ p a g e _ title || t ( ' . t i t l e ' ) %>
</div>

Методу form ta g указывается путь к магазину в качестве страницы, которая


должна быть заново выведена при отправке формы. Атрибут c la s s позволяет нам
связать форму с кодом CSS.
Метод s e le c t_ ta g используется для определения одного поля ввода для этой
формы, а именно lo c a le . Это поле со списком, основанное на значениях массива
LANGUAGES, который определен в файле конфигурации, со значением по умолча­
нию, соответствующим текущему региону (которое также сделано доступным
благодаря модулю 118п). Мы также настраиваем обработчик событий onchange,
который будет отправлять значение этой формы при каждом изменении ее зна­
чения. Этот обработчик будет работать, только если включен JavaScript, но он
очень удобен.
Затем мы добавляем метод su b m it_ tag на тот случай, когда JavaScript отклю­
чен. Для обработки случая, когда JavaScript включен и кнопка отправки данных не
нужна, мы добавляем еще немного кода JavaScript, который скроет каждый из тегов
ввода в форме локализации, даже если мы знаем, что в ней только один такой тег.
Затем мы внесем изменения в контроллер магазина для перенаправления на
путь к магазину, связанный с заданной локализацией, если используется форма
: s e t_ lo c a le :

ra ils3 1 / d e p o t_ t/ a p p / co ritro lle rs / sto re _ co n tro lle r.rb


d ef index
► i f param s[: s e t _ lo c a le ]
► r e d ir e c t _ t o s t o r e _ p a t h (lo c a le : params[ : s e t _ l o c a l e ] )
► e ls e
► (Sproducts = P r o d u c t.o rd e r ( : t i t l e )
► (Slcart = c u r r e n t_ c a r t
► end
End

И в заключение добавим немного CSS-кода:


ra ils3 1 /d e p o t_ t/ a p p /a sse ts/ style sh e e ts/ a p p lica tio n . c s s . scss
. l o c a le {
flo at: r ig h t ;
m argin: -0.2Sem 0.1em;
}
246 Часть II • Создание приложения

Созданный переключатель показан на рис. 15.10. Теперь мы можем переключать


языки в любом направлении простым щелчком мыши.

Р RAG МАТ! С В СУ£ЩSНК»|


Your Pragmatic Catalog

C o ffe e S c rip t
CoffeeScript is Ja v a S c rip t done right It provides all of
JavaScript's functionality' wrapped in a cleaner m ore su ccin ct
syntax, fn the first book on this exciting new language
C o ffeeScrip t guru T revor Burnham show s y o u how to hold onto
all the power and flexibility of J a va S c rip t w hile writing clearer,
cleaner, and safer code.

S3S.CK> A d d TO Cart

Рис, 15.10. Переключатель локализации в верней правой части окна

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

15.5. Наши достижения


К окончанию данного шага нам удалось сделать следующее:
И Мы установили для своего приложения локализацию по умолчанию и предо­
ставили пользователю средства выбора альтернативной локализации.
0 Мы создали файлы переводов для текстовых полей, указания количества
денег, ошибок и имен модели.
0 Мы изменили макеты и представления, чтобы вызвать модуль 118п, исполь­
зуя вспомогательный метод t () с целью перевода текстовых составляющих
интерфейса.

Чем заняться на досуге


Попробуйте проделать все это без посторонней помощи:
О Добавьте столбец l o c a l e к таблице базы данных p r o d u c ts и настрой­
те представление каталога на выбор только тех товаров, которые
Глава 15 • Задача К: локализация 247

соответствуют локализации. Настройте представление товаров, чтобы


можно было просматривать, вводить и изменять значение этого нового
столбца. Введите несколько товаров для каждой локализации и посмотрите,
что получилось.
О Определите текущий курс обмена между американскими долларами и евро
и локализируйте вывод валюты для отображения евро, когда выбрана лока­
лизация ES_es.
О Переведите способы оплаты O r d e r : : PAYMENT_TYPES, показанные в рас­
крывающемся списке. Но при этом выбранное значение (отправляемое
серверу) должно оставаться прежним. Измениться должно только ото­
бражение.
(П одсказки можно найти по адресу http://www.pragprog.com/wikis/wiki/
RailsPiayTime.)
Задача Л:
развертывание
и эксплуатация

О сн о в н ы е тем ы :

> запуск наш его прилож ения на эксп луатаци он н ом веб-сервере;


> конф игурирование базы д ан н ы х для MySQL;
> использование Bundler и G it для управления версиями и
> разверты вание наш его прилож ения с использованием Capistrano.

Развертывание в жизненном цикле приложения знаменует радостную веху. Именно


в этот момент его код, который так бережно создавался, выкладывается на сервер,
чтобы им могли пользоваться другие люди. Предполагается, что пиво и шампанское
должны литься рекой, а стол ломиться от закусок. Вскоре о приложении напишут
в журнале «Wired magazine», а вы сами внезапно станете новой фигурой в избран­
ном обществе.
Но реальность зачастую требует двусторонней проработки, чтобы добиться на­
дежного и повторяемого развертывания вашего приложения.
Когда мы обдумывали содержание данной главы, наша установка имела вид,
показанный на рис. 16.1.
В настоящее время мы проделываем всю работу на одной машине, в то время
как взаимодействие пользователя с нашим веб-сервером должно осуществляться
на отдельной машине. На рисунке машина пользователя находится в центре, а веб­
сервер WEBRick находится слева. На этом сервере используются SQLite3, установ­
ленные вами различные gem-пакеты и код вашего приложения. На данный момент
ваш код может либо находиться на Git-системе, либо нет —в любом случае к концу
данной главы он там будет, а также там будут используемые вами gem-пакеты.
Репозиторий Git будет продублирован на эксплуатационном сервере, кото­
рый опять же может быть совершенно другой машиной, хотя и не обязательно.
Глава 16 • Задача Л: развертывание и эксплуатация 249

Рис. 16.1. Дорожная карта развертывания приложения

На этом сервере будет запущена комбинация, состоящая из Apache httpd и Phusion


Passenger. Этот код будет обращаться к базе данных MySQL, и это может привести
к появлению четвертой машины.
Capistrano будет использоваться для удаленного, безопасного и однообразного
обновления сервера или серверов с развернутым приложением при содействии
нашей разработочной машины.
Здесь немало движущихся частей!
Вместо того, чтобы выполнить все сразу, мы разобьем работу на три шага. Пер­
вый шаг приведет приложение Depot в состояние готовности к работе с Apache,
MySQL и Passenger — настоящим оборудованием веб-сервера, предназначенным
для эксплуатации.
На второй шаг мы оставим работу с Git, Bundler и Capistrano. Эти инструменталь­
ные средства позволят отделить нашу разработочную деятельность от среды развер­
тывания. Это означает, что к моменту, когда все будет сделано, мы проведем развер­
тывание дважды, но это произойдет только в этот —первый —раз. И только для того,
чтобы убедиться, что каждая из частей работает независимо. Это также позволит нам
в любой момент времени сконцентрироваться на меньшем наборе переменных, что
упростит процесс решения любых проблем, которые могут возникнуть.
Третий шаг будет заключаться в решении различных административных задач
и наведении порядка. Итак, приступим!

16.1. Шаг Л1: развертывание


с использованием Phusion Passenger
и MySQL
До сих пор при развертывании Rails-приложения на нашей локальной машине
мы, вероятнее всего, при запуске сервера использовали W EBrick или Mongrel.
250 Часть II • Создание приложения

По большому счету, это не имеет никакого значения. Команда n a i l s s e rv e r была


наиболее подходящим способом запуска нашего приложения в разработочном ре­
жиме на порте 3000. Но развернутое Rails-приложение работает несколько иначе.
Мы не можем просто запустить единственный серверный Rails-процесс и пере­
ложить всю работу на него. Точнее, можем, но эго далеко не идеальное решение.
Причина в том, что Rails является однопоточной средой и может одновременно
работать только с одним запросом.

Д Ж О СПРАШ ИВАЕТ: А НЕЛЬЗЯ ЛИ РАЗВЕРНУТЬ ПРИЛОЖ ЕНИЕ


ПОД MICROSOFT W IN D O W S ? --------------------------------------------- ----------------------
Хотя развертывание приложений в среде W indows вполне допустимо, огромное количество
инструментария Rails и общ едоступного опыта предполагает использование операцион­
ных систем на базе Unix, например Linux или Mac OS X. Одно из таких инструментальных
средств, Phusion Passenger, настоятельно рекомендовано командой разработчиков Ruby on
Rails и будет рассмотрено в данной главе.
Рассматриваемые здесь технологии могут использоваться при такой вот разработке под
W indow s и развертывании под Linux или Mac OS X.

Но Интернет —это сугубо параллельная среда. Эксплуатационные веб-серверы,


такие как Apache, Lighttpd и Zeus, могут одновременно обрабатывать сразу не­
сколько запросов, счет которых может идти на десятки или даже сотни. Имею­
щий всего один процесс однопоточный веб-сервер на базе языка Ruby, наверное,
с этим не справится. К счастью, ему с этим и не нужно справляться. Вместо этого
способом развертывания Rails-приложения и введения его в эксплуатацию станет
использование внешнего сервера, такого как Apache, который будет обрабатывать
запросы, поступающие от клиентов. Затем HTTP прокси-сервер Passenger будет
задействован для отправки запросов, предназначенных для обработки средой Rails,
любому количеству серверных прикладных процессов.

Установка Passenger
Сначала нужно убедиться в том, что установлен и запущен веб-сервер Apache.
У тех, кто пользуется Mac OS X, он уже установлен вместе с операционной систе­
мой, но его нужно включить, перейдя в System Preferences ►Sharing и включив Web
Sharing. Пользователи Linux уже должны были установить Apache в ходе изучения
раздела 1.3 «Установка под Linux».
Следующим шагом будет установка Passenger:
$ gem i n s t a l l passenger
$ pa ssenger-install-apache2-module

Если не будет выявлено никаких необходимых зависимостей, последняя ко­


манда скажет, что нужно делать. Например, при работе под Ubuntu 11.04 (Natty
Narwhal) окажется, что нужно установить libcurl4-openssl-dev, apache2-prefork-dev,
libaprl-dev и libaprutil 1-dev. Если такое произойдет, следуйте предоставляемым ин­
струкциям и запустите команду установки Passenger еще раз.
Глава 16 • Задача Л: развертывание и эксплуатация 251

Как только все зависимости будут удовлетворены, эта команда заставит прове­
сти компиляцию сразу нескольких источников и обновление конфигурационных
файлов. В течение этого процесса нам будут выдаваться запросы на обновление
конфигурации нашего АрасЬе-сервера. Первым будет запрос на включение ваших
только что созданных модулей, и он будет заключаться в добавлении показанных
далее строк к нашей АрасЬе-конфигурации.

П РИ М Е Ч А Н И Е----------------------------------------------------------------------------------------
Passenger подскажет, какие конкретно строки нужно скопировать и вставить в файл, поэто­
му нужно использовать подсказанные им строки, а не те, что показаны здесь. Нам также
пришлось сократить описание пути в строке LoadModule, чтобы оно поместилось на стра­
нице. Воспользуйтесь именно тем описанием пути, которое будет предоставлено Passenger.

LoadModule passengen_module / home/rubys/. rvm/. . ./ext/apache2/m od_passenger.so


PassengerRoot /hom e/rubys/. rvm/gems/ruby- 1 .9 .2-p290/gems/passenger-3.0 .8
PassengerRuby /hom e/rubys/. rvm /w rappers/ruby-1.9 . 2-p290/ruby

Чтобы определить, где именно находится конфигурационный файл Apache,


попробуйте воспользоваться следующей командой:
$ a p a c h e c tl -V I grep HTTPDROOT
$ a p a c h e c tl -V j grep SERVER_CONFIG_FILE
На некоторых системах эта команда называется a p a c h e 2 c t l , на других она на­
зывается h t t p d . Пробуйте, пока не найдете правильную команду.
Если с помощью одного и того же веб-сервера Apache нужно обслуживать сразу
несколько приложений, сначала нужно проверить наличие в конфигурационных
файлах следующей строки:
Nam eVirtualH ost *:80

Если ее нет, добавьте ее перед строкой, содержащей текст L i s t e n 80.

Локальное развертывание нашего приложения


Следующий шаг заключается в развертывании нашего приложения. В то время
как предыдущий шаг выполняется только один раз для каждого сервера, этот шаг
выполняется один раз для каждого приложения. Подставьте в следующую строку
S erv e rN a m e имя своего хоста:
< V irtu a lH o s t *:80>
ServerName depot.yourhost.com
DocumentRoot /hom e/rubys/work/depot/public/
< D ire cto ry /som ew here/pu blio
A llo w O v e rrid e a l l
O ptions -M u ltivie w s
</Directory>
< /V irtu alH cst>

Обратите внимание на то, что параметр DocumentRoot настроен на наш каталог


public в нашем Rails-приложении. Повторите блок V i r t u a l H o s t по одному разу для
252 Часть II • Создание приложения

каждого приложения, настраивая ServerName и DocumentRoot для каждого блока.


Нам также понадобится пометить каталог public доступным для чтения. Оконча­
тельная версия может иметь примерно следующий вид:
< V irtu a lH o s t *:80>
ServerName depot.yourhost.com
DocumentRoot /hom e/rubys/work/depot/public/

< D ire cto ry /hom e/rubys/work/depot/public>


A llo w O v e rrid e a l l
O ptions -M u ltivie w s
Order a llo w ,d e n y
A llo w from a l l
</Directory>
< /VirtualH ost>

Завершающим шагом будет перезапуск вашего веб-сервера Apache:


$ sudo a p a c h e c tl r e s t a r t

Теперь вам нужно будет настроить конфигурацию вашего клиента, чтобы он


отображал выбранное вами имя хоста на правильную машину. Это делается в фай­
ле по имени /etc/hosts. На Windows-машине этот файл можно найти в каталоге С:\
windows\system32\drivers\etc\. Чтобы отредактировать этот файл, вам нужно будет
открыть его, пользуясь правами администратора.
Обычная строка файла /etc/hosts может иметь следующий вид:
1 2 7 .0 .0 .1 depot.yourhost.com

Вот и все! Теперь мы можем обратиться к своему приложению, используя ука­


занный нами хост (или виртуальный хост). Теперь в нашем URL-адресе указывать
номер порта не нужно, если только не используется номер порта, отличный от 80.
Нужно также иметь в виду следующее:
О Если при перезапуске вашего сервера появляется сообщение о том, что адрес
или порт указан неправильно («The address or port is invalid»), это означает,
что строка N a m e V in tu a lH o s t уже присутствует, возможно, в другом конфигу­
рационном файле в том же самом каталоге. Если это так, удалите добавленную
вами строку, потому что эта директива должна присутствовать только один раз.
О Если нужно запустить другую, не эксплуатационную среду, мы можем
включить директиву R a i l s E n v в каждый блок V i r t u a l H o s t в конфигурации
нашего АрасЬе-сервера:
R a ils E n v development

О Мы можем в любое время перезапустить наше приложение без перезапуска


Apache путем создания файла по имени restart.txt в каталоге tmp нашего при­
ложения:
$ touch t m p / r e s t a r t .tx t

Как только сервер будет перезапущен, этот файл будет удален.


Глава 16 • Задача Л: развертывание и эксплуатация 253

О Вывод команды p a s s e n g e r - in s ta ll- a p a c h e 2 - m o d u le сообщит нам, где


можно найти дополнительную документацию.

Использование MySQL в качестве базы данных


На веб-сайте SQLite1на удивление честно рассказано, для каких целей лучше при­
менять эту базу данных и на что она неспособна. В частности, SQLite не рекомен­
дуется для объемных веб-сайтов с большим количеством параллельных запросов
и большими наборами данных. И, разумеется, мы хотим, чтобы у нашего веб-сайта
были именно такие параметры.
Существует масса альтернатив базе данных SQLite, как бесплатных, так и ком­
мерческих. Мы будем использовать MySQL. Она доступна из вашего исходного
пакета инструментов в Linux и предоставляется для OS X в виде установочного
пакета на веб-сайте MySQL2. Мы рекомендуем загрузить MySQL 5.1, поскольку
известно, что MySQL 5.5 с Rails 3.1 не работает.
Кроме установки базы данных MySQL, вам также понадобится добавить
в Gemfile указание на gem-пакет mysql:

rails31/depot_t/Gemfile
group : p ro d uction do
gem 'm ysql2'
end

Если этот gem-пакет помещен в группу p ro d u c tio n , он не будет загружаться


при запуске приложения в режиме разработки или тестирования. Если хотите,
можете поместить gem-пакет s q l i t e 3 в отдельные группы разработки и тестиро­
вания — developm ent и t e s t .
Установите gem-пакет с помощью команды bundle i n s t a l l . Сначала может
потребоваться найти и установить разработочные файлы базы данных MySQL
для вашей операционной системы. К примеру, при работе под Ubuntu понадобится
установитьlib m y s q lc lie n t- d e v .
Для создания своей базы данных можно воспользоваться клиентом командной
строки mysql или, если вам больше нравится такое средство, как phpmyadmin или
CocoaMySQL, воспользуйтесь им:
depot> m ys q l -u r o o t
mysql> CREATE DATABASE d e p o t _ p r o d u c t i o n ;
mysql> GRANT ALL PRIVILEG ES ON d e p o t _ p r o d u c t i o n .*
-> TO ' u s e r n a m e ' @ ' l o c a l h o s t ' IDENTIFIE D BY 'p a s sw o rd ';
mysql> EXIT;

Если вы выбрали другое имя базы данных, его следует запомнить, поскольку
вам понадобится настроить конфигурационный файл на соответствие выбранному
имени. Теперь давайте взглянем на конфигурационный файл.

1 http://www.sqlite.org/whentouse.html
2 http://dev.mysql.com/downloads/mysql/
254 Часть II • Создание приложения

Файл config/database.yml содержит информацию о подключениях баз данных.


Он состоит из трех разделов, каждый из которых предназначен для баз данных
разработки, тестирования и эксплуатации. Текущий раздел эксплуатации имеет
следующее содержимое:
p ro d u c tio n :
a d a p te r: s q lit e 3
d atabase: d b /p ro d u ctio n . s q lit e 3
p o o l: 5
tim e o u t: 5000

Мы заменим этот раздел чем-нибудь таким:


p ro d u c tio n :
a d a p te r: mysql
encoding: u tf8
re co n n e ct: f a ls e
d atab ase: depot_production
p ool: 5
username: username
password: password
h o st: lo c a lh o s t

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

Загрузка базы данных


Далее мы применим наши миграции:
depot> r a k e d b : s e t u p R A IL S _ E N V = "p r o d u c tio n "

При этом произойдет одно или два события. Если все настроено правильно, вы
увидите примерно такой вывод:
-- c r e a t e _ t a b l e ( " c a r t s " , { :fo rc e = > tru e })
-> 0 .1 7 2 2 s
-- c r e a t e _ t a b le ( " lin e _ it e m s " , { :fo rc e = > tru e })
-> 0 .1 2 5 5 s
-- c r e a t e _ t a b le (" o r d e r s " , { :fo rc e = > tru e })
-> 0 .1 1 7 1 s
-- c r e a t e _ t a b le (" p r o d u c t s " , { : fo rce = > tru e })
-> 0 .1 1 7 2 s
-- c r e a t e _ t a b le (" u s e r s ” , { : fo rce = > tru e })
-> 0 .1 2 5 5 s
- - in it ia liz e _ s c h e m a _ m ig r a t io n s _ t a b le ()
-> 0 .0 0 0 6 s
-- assum e_migrated_upto_version(20110711000008, "d b /m ig ra te ")
-> 0 .0 0 0 8 s

Если вместо этого вы увидите какие-нибудь ошибки, не нужно паниковать! Это,


скорее всего, простые проблемы конфигурации. Можете попробовать следующее:
О Проверьте имя, которое вы дали базе данных в разделе p r o d u c tio n : файла
database.yml. Оно должно быть таким же, как и имя созданной вами базы
Глава 16 • Задача Л: развертывание и эксплуатация 255

данных (с помощью mysqladmin или какого-нибудь другого средства адми­


нистрирования базы данных).
О Проверьте, что имя пользователя и пароль в файле database.yml соответству­
ют тем, которые были использованы при создании базы данных.
О Проверьте, запущен ли сервер вашей базы данных.
О Проверьте возможность подключения к ней из командной строки. Если ис­
пользуется MySQL, запустите следующую команду:
d e p o t > mysql depot__production
mysql>

О Если подключение из командной строки возможно, можете ли вы создать


таблицу-пустышку dummy? (Так можно будет проверить, имеет ли пользова­
тель базы данных достаточные права доступа к базе данных.)
my sql> c re a te t a b le dummy(i i n t ) ;
my sql> drop t a b le dummy;

О Если вы можете создавать таблицы из командной строки, а команда гаке


d b : m ig ra te не проходит, еще раз проверьте файл database.yml. Если в файле
есть директивы s o c k e t:, попробуйте их закомментировать, поставив перед
каждой символ решетки (#).
О Если вы видите ошибку отсутствия указанного файла или каталога («No such
file or directory...») и имя файла, вызвавшего ошибку mysql.sock, значит, ваши
Ruby-библиотеки MySQL не могут найти вашей базы данных MySQL. Это
может случиться, если вы установили библиотеки до установки базы данных
или если установили библиотеки, используя двоичный пакет установки,
который сделал неверное предположение о месте нахождения сокет-файла
MySQL. Чтобы исправить положение, лучше всего переустановить Ruby-
библиотеки MySQL. Если это неподходящий вариант, еще раз проверьте, что
в строке s o c k e t: в вашем файле database.yml содержится правильный путь
к MySQL-сокету на вашей системе.
О Если получена ошибка отсутствия загрузки MySQL (Mysql not loaded), это
означает, что вы запустили старую версию Ruby-библиогеки MySQL. Среде
Rails нужна как минимум версия 2.5.
О Некоторые читатели также сообщают о получении сообщения о том, что кли­
ент не поддерживает протокол аутентификации, запрашиваемый сервером
(«Client does not support authentication protocol requested by server»), что пред­
полагает обновление MySQL-клиента. Чтобы избавиться от этой несовмести­
мости установленной версии MySQL и библиотек, используемых для обраще­
ния к этой базе данных, выполните инструкции, имеющиеся на веб-сайте http://
dev.mysql.com/doc/mysql/en/old-client.html и запустите MySQL-команду вроде s e t
password f o r ' s o m e _ u s e r s o m e _ h o s t' = OLD__PASSWORD(‘newpwd’ ) ;.
О Если вы используете в Windows MySQL под Cygwin, могут появиться про­
блемы, если указать хост как lo c a lh o s t. Попробуйте вместо этого исполь­
зовать 1 2 7 .0 .0 .1 .
256 Часть И • Создание приложения

О И наконец, у вас могут появиться проблемы в формате файла database.yml.


Библиотека YAML, которая читает этот файл, очень странно реагирует на
символы табуляции.
Если в вашем файле содержатся знаки табуляции, ждите проблем. (А вы ду­
мали, что предпочли язык Ruby языку Python, потому что вам не нравилась
имеющаяся в Python значимость пробельных символов?)
Запустите команду rake db:setup столько раз, сколько понадобится для исправ­
ления любых имеющихся проблем конфигурации.
Если вас это пугает, не стоит переживать. В действительности подключение к
базе данных в основном работает безотказно. И как только Rails сможет работать
с базой данных, вам не придется больше об этом беспокоиться.
Ну, все готово к работе. Все выглядит точно так же, как и при запуске в одно­
пользовательском режиме. Разница становится заметной только тогда, когда у вас
появляется большое количество одновременно обращающихся пользователей или
когда база данных становится слишком большой.
Следующий шаг заключается в обособлении нашей разработки от нашей экс­
плуатационной машины.

16.2. Шаг Л2: удаленное развертывание


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

Подготовка вашего сервера развертывания


Хотя помещение нашей программы во время ее разработки под управление версия­
ми —действительно весьма неплохая идея, когда дело доходит до развертывания,
то отказ от использования управления версиями превращается в абсолютное без­
рассудство. Достаточно сказать, что выбранная для управления развертыванием
программа, а именно Capistrano, практически требует использования этого управ­
ления.
На данный момент существует множество систем управления версиями программ
(software configuration management — SCM). Например, есть достаточно хорошая
система Subversion. Но, если вы еще не сделали свой выбор, остановитесь на системе
Git, которую легко установить и которая не требует отдельного серверного процес­
са. Показанные далее примеры основаны на использовании системы Git, но, если
Глава 16 • Задача Л: развертывание и эксплуатация 257

вы выбрали другую SCM-систему, не стоит переживать. Capistrano в принципе все


равно, какую именно систему вы выбрали, лишь бы она была выбрана.
Первый шаг заключается в создании пустого репозитория на машине, доступ­
ной вашим серверам развертывания. Фактически, если у нас только один сервер
для развертывания приложения, ничто не препятствует тому, чтобы он выполнял
двойную обязанность, будучи еще и вашим G it-сервером. Итак, войдите в этот
сервер и запустите следующие команды:
$ mkdir -р ~ / g it/d ep o t.g it
$ cd ~ / g it/d ep o t.g it
$ g it --bare i n i t

Следующее, что вам нужно знать: даже если SCM -сервер и ваш веб-сервер
являются одной и той же физической машиной, Capistrano будет обращаться к на­
шему программному обеспечению SCM так, как будто оно находится на удаленной
машине. Мы можем сгладить это путем генерации открытого ключа (если у вас еще
нет такого ключа) с его последующим использованием для получения разрешения
на доступ к своему собственному серверу:
$ te s t -е ~/.ssh/id_dsa.pub || ssh-keygen -t dsa
$ cat ~/.ssh/id_dsa.pub >> ~/. ssh/authorized_keys2

Протестируйте этот метод путем использования безопасного сетевого протоко­


ла SSH на своем собственном сервере. Кроме всего прочего, это гарантирует нам
обновление вашего файла known_hosts.
И, пока мы здесь, нужно проследить еще за одной вещью. Capistrano вставит
каталог по имени current между каталогом с именем нашего приложения и под­
каталогами Rails, включая и подкаталог public. Это означает, что вам нужно внести
поправки в DocumentRoot в вашем файле httpd.conf, если вы управляете своим
собственным сервером, или в панель управления для вашего общего хоста:
DocumentRoot /hom e/rubys/w ork/depot/current/public/

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

Получение контроля над приложением


Сначала мы собираемся обносить наш Gemfile, чтобы показать, что мы используем
Capistrano.

rails31/depot_t/Gemfile
source ' h ttp :// ru b yg em s.o rg '

gem ' r a i l s ' , '3 .1 .0 '

# Bundle edge R a ils in s te a d :


# gem ' r a i l s ' , : g i t => ' g i t : / / g i t h u b . c o m / r a i l s / r a i l s . g i t '

gem 's q l i t e 3 '


258 Часть II • Создание приложения

group p ro d u c tio n do
gem 'm ysql2'
end

# Gems used o n ly f o r a s s e ts and not req u ire d


# in pro d uctio n environm ents by d e f a u lt ,
group ra sse ts do
gem ' s a s s - r a i l s ', " ~> 3 .1 .0 "
gem ’ c o f f e e - r a i l s ' , "~> 3 .1 .0 "
gem 'u g l if ie r '
end

gem ' j q u e r y - r a i l s '

# Use u n ico rn as the web s e rv e r


# gem 'u n ic o rn '

# Deploy w ith C ap istran o


►gem 'c a p is t r a n o '

# To use debugger
# gem ' ruby-debugl9 ' , .'re q u ire => 'ruby-debug'

group : t e s t do
# P r e t t y p rin te d t e s t output
gem ' t u r n ' , : re q u ire => f a ls e
end
gem 'w i l l _ p a g i n a t e ', '~> 3 .0 '

Теперь мы можем установить Capistrano, используя b u n d le i n s t a l l . Мы


использовали эту команду в главе 12, в шаге ЖЗ для установки gem-пакета will_
paginate.
Если вы еще не поставили ваше приложение под контроль версий, сделайте
это сейчас:
$ cd y o u r _ a p p l i c a t i o n _ d i r e c t o r y
$ g it in it
$ g i t add .
$ g i t commit -m " i n i t i a l commit"

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


у вас нет полного контроля над сервером развертывания или если у вас большое
количество серверов развертывания, которые нужно контролировать. Мы собира­
емся воспользоваться вторым свойством программы Bundler, а именно командой
pack. Она поместит версию программного обеспечения, от которого вы зависите,
в репозиторий:
$ b u n d le pa ck
$ g i t add G e m f ile . l o c k v e n d o r / c a c h e
$ g i t commit -m " b u n d le gems"

Дополнительные свойства программы Bundler будут рассмотрены в разделе 25.3


«Управление зависимостями с помощью Bundler».
Глава 16 • Задача Л: развертывание и эксплуатация 259

Отсюда будет проще выложить весь ваш код на сервер:


$ g it remote add o rig in ssh://user@ host/~/git/depot.git
$ g it push o rig in master

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

Удаленное развертывание приложения


Ранее мы уже развертывали приложение локально на сервере. Теперь мы собира­
емся провести второе развертывание, на этот раз удаленное.
Вся подготовительная работа уже проведена. Теперь наш код находится на
SCM-сервере, где он может быть доступен серверу приложения. Здесь снова имеет
значение не то, являются ли эти оба сервера одной и той же машиной, а то, какие
роли они выполняют.
Чтобы заставить Capistrano совершить волшебство и добавить необходимые
для проекта файлы, запустите следующую команду:
$ c a p ify .
[add] w r it in g './ C a p file '
[add] w r it in g ’ ./co n fig /d ep lo y.rb '
[done] capified!

Из выведенной информации видно, что программа Capistrano установила два


файла. Первый — Capfile —это принадлежащий Capistrano аналог Rakefile. Больше
мы этого файла касаться не будем. Второй — config/deploy.rb — содержит наборы
команд, необходимые для развертывания нашего приложения. Capistrano предо­
ставляет нам минимальную версию этого файла, но следующий файл является
более сложной версией, которую вы можете загрузить и использовать в качестве
отправной точки:

rails31/depot_t/Capfile
load 'd e p lo y ' i f re s p o n d _ to ?(: namespace) # cap2 d i f f e r e n t i a t o r

# Uncomment i f you are using R a i l s ' a s s e t p ip e lin e


►load 'd e p lo y / a s s e ts '

D i r [ ' v e n d o r/ g e m s / */ re c ip e s / * .rb ', ' v e n d o r/ p lu g in s / * / re c ip e s / * . r b ' ] .


each { |p lu g in | lo a d (p lu g in ) }

load ' config/deploy’ # remove t h i s li n e to s k ip lo a d in g any o f th e d e f a u lt ta s k s

rails31/depot_t/config/deploy.rb
# be sure to change these
s e t :u s e r, 'ru b y s '
s e t idomainj 'depot.pragprog .com '
260 Часть II • Создание приложения

s e t : a p p lic a t io n , 'd e p o t'

# a d ju s t i f you are using RVM, remove i f you are not


$ : . u n s h i f t ( F i le . e x p a n d _ p a t h ( '. / li b ', E N V ['rv m _ p a th '] ) )
re q u ire "rv m /c a p istra n o "
se t : rv m _ ru b y _ strin g , '1 .9 . 2 '
se t :rvm _type, :u ser

# file paths
se t :r e p o s it o r y , "# {u s e r }@ # {d o m a in }:g it / # {a p p lic a t io n }.g it "
se t :d e p lo y _ to , "/h om e/#{user}/# {dom ain}"

# d is t r ib u t e your a p p lic a tio n s acro ss s e rv e rs (th e in s t r u c t io n s below put them


# a l l on th e same s e r v e r , defined above as 'd o m ain ', a d ju s t as n e c e ssa ry)
r o le :app, domain
r o le :web, domain
r o le :db, domain, :p rim ary => tru e

# you might need to se t t h i s i f you a r e n 't seeing password prompts


# d e f a u lt _ r u n _ o p tio n s [:p ty ] = tru e
# As C a p istra n o executes in a n o n - in te r a c tiv e mode and th e r e fo re d o e s n 't cause
# any o f your s h e ll profile s c r ip t s to be run, th e fo llo w in g might be needed
# i f ( f o r example) you have l o c a l l y in s t a l le d gems or a p p lic a t io n s . Note:
# t h i s needs to c o n ta in th e f u l l va lu e s f o r the v a r ia b le s s e t , not sim ply
# th e d e lta s .
# d e fa u lt_ e n v ir o n m e n t[’ PATH' ] = ' <your paths> : / u s r / lo c a l/ b in : / u s r / b in : / b in ’
# d e fa u lt_ e n v iro n m e n t[ ’GEM_PATH' ] = ' <your p ath s>:/u s r/lib / ru b y/ g em s/1 .8 '
# m isce llan e o u s o p tio n s
s e t : d e p lo y _ v ia , : remote_cache
s e t :scm, ' g i t '
se t :b ran ch, 'm a ste r'
s e t : scm _verbose, tru e
s e t :use_sudo, f a ls e
s e t : r a il s _ e n v , p ro d u c tio n

namespace :d e p lo y do
desc "cause Passenger to i n i t i a t e a r e s t a r t "
ta s k : r e s t a r t do
run "touch # {c u r r e n t_ p a th }/ tm p / r e s t a r t .tx t "
end

desc "r e lo a d th e database w ith seed d a ta "


ta s k :seed do
run "cd # {c u r r e n t_ p a th }; rake db:seed R A IL S _ E N V = # {ra ils _ e n v }"
end
end

a f t e r "d e p lo y :u p d a te _c o d e ", : b u n d le _ in s t a ll
desc " i n s t a l l th e n ecessary p r e r e q u is ite s "
ta s k : b u n d le _ in s t a ll, :r o le s => :app do
run "cd # {r e le a s e _ p a th } && bundle i n s t a l l "
end

Чтобы привести все в соответствие с нашим приложением, нам нужно будет от­
редактировать несколько свойств. Нам, конечно, понадобится изменить свойства
Глава 16 • Задача Л: развертывание и эксплуатация 261

:u se r, : domain и : a p p lic a tio n . Свойство r e p o s i t o r y соответствует месту, куда


мы ранее поместили Git-файл. Свойство : deploy_to может потребовать настройки
на соответствие той информации, которую мы сообщили серверу Apache (о том,
где он может найти каталог config/public для приложения).
Мы также включили несколько строк, чтобы показать, как нужно проинструк­
тировать программу Capistrano, чтобы она воспользовалась RVM1. Если диспетчер
версии Ruby (RVM) был установлен на вашей машине развертывания как корневой,
удалите строку s e t : rvm_type. Внесите изменение в строку : rvm _ruby_string,
чтобы она соответствовала версии интерпретатора Ruby, которую вы установили
и хотите использовать. Если вы вообще не используете RVM, удалите эти строки.
Настройки d ef a u lt_ ru n _ o p tio n s и d ef a u lt_ e n v iro n m e n t должны использо­
ваться только тогда, когда у вас возникли специфические проблемы. Предоставлен­
ные смешанные настройки («miscellaneous options») основаны на Git. Определены
две задачи: одна сообщает Capistrano, как перезапустить Passenger, другая устанав­
ливает gem-пакеты из копии, которую мы ранее поместили в Git-репозиторий. Вы
можете изменять эти задачи по собственному усмотрению.
При первом развертывании нашего приложения нам необходимо выполнить
дополнительные шаги для настройки основной структуры каталогов развертыва­
ния на сервере:
$ cap d e p lo y:s e tu p

При выполнении этой команды Capistrano выведет приглашение на ввод паро­


ля сервера. Если программа этого не сделает и не сможет войти в систему, может
понадобиться убрать символ комментария со строки d e f a u lt_ ru n _ o p tio n s в на­
шем файле deploy, rb и повторить попытку. Как только появится подключение, не­
обходимые каталоги будут созданы. После выполнения этой команды мы можем
проверить нашу конфигурацию на наличие любых других проблем:
$ cap d ep lo y:ch eck

Как и раньше, нам может потребоваться убрать символы комментария и настро­


ить строки d e fa u lt_ e n v iro n m e n t в нашем файле deploy.rb. Мы можем повторять
эту команду до тех пор, пока она не будет успешно завершена, решая любые про­
блемы, которые она сможет выявить.
Теперь мы готовы осуществить развертывание. Поскольку вся подготовительная
работа уже проведена и результаты проверены, все должно пройти гладко:
$ cap d e p lo y :m ig ra tio n s

На этом гонки должны закончиться.

Прополоскать. Промыть. Повторить


Если вы добрались до этих строк, значит, сервер в любое время готов заполу­
чить новую версию развернутого на нем приложения. Нужно только проверить,

1 http://beginrescueend.com/integration/capistrano/
262 Часть II • Создание приложения

внесены ли все необходимые изменения в версию, находящуюся в репозитории, а


затем провести повторное развертывание. К этому моменту у нас есть два еще не
проверенных файла Capistrano. Хотя сервер приложений в них не нуждается, мы
все же можем воспользоваться ими для проверки процесса развертывания:
$ g it add .
$ g it commit -m "add cap file s "
$ g it push
$ cap deploy

Первые три команды проведут обновление SCM -сервера. Когда вы познако­


митесь с Git поближе, вам может потребоваться более строгий контроль того,
когда и какие файлы добавляются, вам может понадобиться до развертывания
передать с наращиванием сразу несколько изменений, и т. д. Но только послед­
няя команда обновляет серверы нашего приложения, а также веб-сервер и сервер
базы данных.
Если по каким-то причинам нам понадобится отступить и вернуться к предыду­
щей версии нашего приложения, можно воспользоваться этой командой:
$ cap d e p lo y :r o llb a c k

Теперь у нас есть полностью развернутое приложение, и мы можем при не­


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

16.3. Ш аг/13: проверка работы


развернутого приложения
Когда приложение будет развернуто, вам, несомненно, понадобится периодически
проверять, как оно работает. Для этого есть два основных способа. Первый из них
заключается в отслеживании содержимого различных файлов, содержащих реги­
страционные журналы, заполняемые как внешними веб-серверами, так и сервером
Apache, запустившем наше приложение. Второй способ состоит в подключении
к приложению с помощью команды r a i l s console.

Просмотр регистрационных файлов


Чтобы взглянуть, что происходит в приложении, можно использовать команду
t a i l , чтобы изучить регистрационные файлы на наличие запросов, сделанных
в адрес нашего приложения. Обычно самые интересные данные находятся в реги­
страционных журналах самого приложения. Даже если Apache запускает сразу не­
сколько приложений, регистрируемый вывод для каждого приложения помещается
в файл production.log для этого приложения.
Глава 16 • Задача Л: развертывание и эксплуатация 263

Предполагая, что наше приложение развернуто в том месте, которое мы по­


казывали ранее, на наши запущенные регистрационные файлы можно взглянуть
следующим образом:
# На нашем сервере
$ cd /home/rubys/work/depot/
$ t a i l -f log/production.log

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

Использование консоли для наблюдения


за работающим приложением
В классах моделей приложения создано большое количество разнообразных
функций. Разумеется, все они были созданы для использования контроллерами
приложения. Но с ними можно общаться и напрямую. Воротами в этот мир служит
сценарий консоли Rails. Мы можем запустить его на нашем сервере с помощью
следующих команд:
# На нашем сервере
$ cd /home/rubys/work/depot/
$ r a i l s console production
Loading p ro d uction environm ent.
irb (m a in ):0 0 1 :0 > p = P r o d u c t.fin d _ b y _ title ("P r a g m a tic V e rsio n C o n tr o l")
=> #<Product:0x24797b4 (S )a ttrib u te s= {. . . }
irb (m a in ):0 0 2 :0 > p .p r ic e = 32.95
=> 32.95
irb (m a in ):0 0 3 :0 > p .save
=> tr u e

Поскольку у нас есть открытый сеанс работы с консолью, мы можем обращаться


ко всему разнообразию методов в наших моделях. Мы можем создавать, проверять
и удалять записи. В некотором смысле это похоже на пульт управления приложе­
нием.
После запуска приложения в эксплуатацию нам нужно позаботиться о не­
скольких рутинных операциях, чтобы наше приложение работало без сбоев. Эти
рутинные операции не будут выполняться за нас автоматически, но, к счастью, мы
можем их автоматизировать.

Работа с регистрационными файлами


При работе приложения в его регистрационном файле происходит постоянное
накопление информации. Со временем регистрационные файлы могут разбухать
до невероятных размеров. Для решения этой проблемы многие регистрационные
решения способны преобразовывать регистрационные файлы для создания по­
следовательного набора регистрационных файлов в хронологически возрастающем
264 Часть II • Создание приложения

порядке. При этом регистрационные файлы разбиваются на удобные для исполь­


зования фрагменты, которые могут быть помещены в архив или даже удалены по
прошествии некоторого времени.
Преобразование поддерживается классом Logger. Нам требуется определить,
сколько регистрационных файлов нам нужно (или как часто они нужны) и размер
каждого файла, используя строку в файле config/environments/production.rb, похожую
на следующую:
co n fig.lo gger = L o g g e r.n e w (c o n fig .p a th s[' lo g ' ] .f ir s t , 'd a i ly ')

Или, возможно, этот код:


re q u ire ' a c tiv e _ s u p p o rt/ c o re _ e x t/ n u m e ric / b y te s '
co n fig.lo gger = Logger. new (config.p aths[ ' lo g ' ] .f ir s t , 10, 1 0 .megabytes)

Учтите, что в данном случае требуется явный запрос a c tiv e _ su p p o rt, посколь­
ку эта инструкция обрабатывается на ранней стадии при инициализации вашего
приложения, до того как были включены библиотеки Active Support. Фактически,
одна из настроек конфигурации, предоставляемая Rails, заключается в полном от­
казе от включения библиотек Active Support:
c o n fig .a c tiv e _ s u p p o rt.b a re = tr u e

Вместо этого мы можем направить наши регистрационные записи в системные


журналы для нашей машины:
config.logger = S y slo g L o g g e r. new

Дополнительные настройки можно