- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF109:Flickr
Материал из Linuxformat.
- Rails Устройте на своем сайте личную фотогалерею с нужными вам функциями
Содержание |
Rails: Добавим функции Web 2.0
- Часть 2 Простейшие возможности галереи реализованы; Алекс Янг приобщит вас к Web 2.0 через супер-технологии Ajax и вспомогательные классы Rails.
На данном этапе наша галерея на Rails поддерживает регистрацию пользователей и загрузку фотографий (считая, что вы прошли урок прошлого номера), но в ней кое-чего не хватает – нет нумерации страниц, нет модульного тестирования и нет Ajax, столь любимого Web 2.0-сайтами типа Flickr. Чтобы восполнить пробелы, я расскажу вам о тестировании моделей Rails, использовании вспомогательных классов Rails и крутых технологиях JavaScript, делающих интерфейс более дружелюбным.
Данный урок основан на коде из предыдущей статьи, найти который можно в PDF-файле в разделе Журнал/Rails нашего DVD. Класс редактирования на месте средствами JavaScript может пригодиться вам и в других проектах.
Часть 1 Тесты
Тестирование – неотъемлемая часть процесса разработки на Ruby on Rails. Написание тестов во время изучения Rails также помогает исследовать эту среду, экспериментировать с различными подходами и в конечном счете получить лучший код. Создатели Rails вложили в пакет все необходимое для обучения тестированию, включая несколько задач Rake. До запуска тестов выполните из каталога проекта следующую команду:
rake db:test:prepare
Она подготовит базу данных SQLite в каталоге db/test.sqlite3 и скопирует схему для нашего проекта. Если вы измените БД посредством миграции, не забудьте выполнить команду rake db:test:clone_structure до прогона тестов.
Чтобы запустить все тесты, наберите:
rake test:units
Когда вы создаете модели и контроллеры с помощью скрипта generate, Rails автоматически генерирует заглушки для тестов в каталоге test/. Модульные тесты предназначены для моделей, а функциональные – для контроллеров. Первые позволяют проверить, что модели работают как ожидается. Это невероятно удобно, если проект меняется с течением времени: при модификации модели можно убедиться, что сопутствующая система не затронута.
Для тестовых данных Rails использует наработки [fixtures]. По умолчанию они пишутся на YAML и хранятся в каталоге test/fixtures/. Наработки могут даже содержать фрагменты на Ruby, как HTML-шаблоны Rails:
quentin: id: 1 login: quentin email: quentin@example.com salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b 1 # test created_at: <%= 5.days.ago.to_s :db %>
Пример взят из test/fixtures/users.yml – обратите внимание, что мы добавили дату при помощи Ruby, с тэгами <%= %>.
Покамест фотографии добавляются без заголовков – а какая же галерея без них? Чтобы решить проблему, добавим новую валидацию в Photo и протестируем ее. Откройте файл app/models/photo.rb и впишите в него строку:
validates_presence_of :title, :if => Proc.new { |photo| photo.thumbnail.nil? }
Валидация включает блок проверки, не является ли фото миниатюрой – потому что они спокойно сохраняются и без заголовков: так уж устроен модуль attachment_fu. Давайте проверим, что все работает как надо: откройте файл test/unit/photo_test.rb. Мы добавим туда несколько новых тестов и удалим заглушку, сгенерированную Rails:
require File.dirname(__FILE__) + ‘/../test_helper’ class PhotoTest < ActiveSupport::TestCase fixtures :photos def test_titles_are_required_on_create assert Photo.create.errors.on(:title) end def test_titles_are_required_on_update photo = photos(:one) photo.title = nil photo.save assert photo.errors.on(:title) end def test_titles_are_not_required_for_thumbnails assert photos(:thumbnail).valid? end end
Обратите внимание, что все тестовые методы имеют префикс test_ и включают один или несколько вызовов assert. Все методы, имя которых начинается с test_, запускаются тестовым пакетом автоматически.
Теперь нам недостает лишь распознавания миниатюр. Откройте файл test/fixtures/photos.yml и дополните его следующим кодом YAML:
thumbnail: description: MyText filename: MyString content_type: MyString size: 1 user_id: 1 width: 1 height: 1 thumbnail: example.png content_type: image/png
Чтобы тест завершился успешно, все утверждения [assertions] должны быть истинными. Многие из них предоставляются как Rails, так и библиотекой Ruby Test::Unit. Мы взяли простейшее: оно проверяет, что выражение не возвращает nil или false. Строка с fixtures использует файл test/fixtures/photos.yml для создания фотографий в базе данных. Доступ к «наработке» обеспечивается методом photos() – он любезно сгенерирован для нас. В примере выше используется photos(:one). Тесты фото вызываются командной строкой ruby test/unit/photo_test.rb, а можно запустить все модульные тесты сразу, набрав команду
rake:test:units.
Часть 2 Ajax и вспомогательные классы Rails
Использование блоков respond_to в контроллерах позволяет Rails разумно отвечать на запросы различных форматов. Они лежат в основе XML Rest API и респондеров Ajax.
Нашим приложением было бы проще пользоваться, имей оно пару
функций, реализованных на JavaScript. Rails предоставляет богатый
функционал для работы с JavaScript через каркас Prototype и библиотеку Scriptaculous. Обработка текста и HTML реализуется библиотеками Ruby ActiveSupport и ActionView, поставляемыми с Rails.
Воспользуемся вспомогательными классами Rails для создания удобных дат, а также заголовков и описаний с редактированием на месте, как во Flickr. Выводом при просмотре тоже будут управлять вспомогательные классы. На прошлом уроке вы могли заметить вызовы link_to, image_tag и form_for. Эти методы предоставляются различными вспомогательными классами внутри ActionView::Helpers. Прежде чем самим писать вспомогательные функции, всегда полезно проверить, нет ли их
в ActionView, так как в Rails их сотни.
В нашей галерее фотографии отображаются без всяких дат, поэтому откройте файл, касающийся фото (app/views/photos/_photo.html.erb), и добавьте следующую строку после тэга заголовка <h2>:
<h3>Added on <%= photo.created_at %></h3>
К сожалению, это выглядит не очень хорошо. Дата содержит информацию о времени и часовом поясе, а хватило бы только числа, месяца и года. Измените эту строку на такую:
<h3>Added on <%= photo.created_at.to_date.to_s :long %></h3>
Теперь дата отображается в британском полном формате: Месяц День, Год. Это работает благодаря методу Rails to_formatted_s, с синонимом to_s. И даты, и времена работают подобным образом, поэтому значение, сохраняемое в created_at, преобразуется в дату, а время размещения фото игнорируется.
Чтобы узнать больше о форматах даты и о том, как добавить свой собственный, выполните следующую команду для вывода документации Ruby по теме:
ri ActiveSupport::CoreExtensions::Date::Conversions
Живые заголовки
Хотя функция «Редактировать на месте» выглядит весьма продвинутой, реализовать ее довольно просто. Большую часть работы сделает библиотека Scriptaculous, по умолчанию поставляемая с Rails, но PhotosController нужно немного адаптировать, чтобы он смог работать с тем, чего ожидает Scriptaculous. Во-первых, нужно приспособить действие PhotosController#update для работы с JavaScript. Отредактируйте действие обновления в файле app/controllers/photos_controller.rb:
def update @photo = current_user.photo respond_to do |format| if @photo.update_attributes params[:photo] format.html do flash[:notice] = ‘Photo was successfully updated.’ redirect_to(@photo) end format.xml { head :ok } format.js do if params[:field] render :inline => ‘<%= h photo.attributes[params[:field]]} %>’ else head :ok end end else format.html { render :action => “edit” } format.xml { render :xml => @photo.errors, :status => : unprocessable_entity } format.js { render :text => @photo.errors.full_messages, :status => :unprocessable_entity } end end end
Этот код делает кое-что новое. Обработка запросов JavaScript поддерживается в format.js в блоке respond_to. Когда фотография успешно сохранена и используется JavaScript, выводится заголовок, поскольку Ajax.InPlaceEditor, предоставляемый библиотекой Scriptaculous, ожидает, что сервер возвратит обновленный текст. Обратите внимание, что для этого используется render :inline, позволяя контроллеру вызвать вспомогательный метод. Текст экранируется, чтобы избежать опасных метасимволов. Также добавьте в начало контроллера (после строки before_filter) следующую строку:
skip_before_filter :verify_authenticity_token, :only => [ :update ]
По соображениям безопасности в Rails используется механизм аутентификации, порождающий проблемы при обновлении с помощью JavaScript, и пока мы его отключим.
Позаботимся о редактировании
Нам надо, чтобы редактирование на месте предоставлялось только зарегистрированным пользователям, но чтобы проверить это, не будем забивать наш файл выражениями с if, а создадим новый вспомогательный класс. Откройте файл app/helpers/application_helper.rb и добавьте следующий метод:
def in_place_editor_if(condition, object, field, &block)
if condition
object_id = “#{object.class.name.downcase}_#{field}_#{object.id}”
in_place_class = “in_place_#{field}”
concat ‘’ % [object_id, in_place_class], block.binding
yield
concat ‘’, block.binding
else
yield
end
end
Это довольно-таки продвинутый Ruby – ничего страшного, если понять его сейчас трудно, но обязательно вернитесь к нему позже! Его нужно использовать примерно так:
<% in_place_editor_if current_user, photo, :title do %><%= h photo.title %><% end %>
Если переменная current_user установлена, заголовок фото будет обернут в тэг <span> с атрибутами id и class, для упрощения ссылок на него из JavaScript. У незарегистрированных пользователей заголовок будет выводиться без тэга <span>, а значит, JavaScript его проигнорирует. В данном фрагменте кода заголовок упрятан в блок с синтаксисом скобок do ... end. Таким образом, этот вспомогательный класс может инкапсулировать все, даже многострочный HTML. Внутри вспомогательного класса для доступа к этому HTML-коду и добавления в него дополнительного текста вызываются yield и concat.
Хотя это уже высший пилотаж, настоятельно рекомендую вам применять его и в других вспомогательных методах, которые вы соберетесь писать. Отредактируйте файл app/views/photos/_photo.html.erb, чтобы употребить в нем этот хитрый вспомогательный класс для изменения способа отображения заголовков:
<h2> <% in_place_editor_if current_user, photo, :title do %><%= h photo.title %><% end %> <%= link_to ‘Edit’, edit_photo_path(photo) %> <%= link_to ‘Delete’, photo, :confirm => ‘Are you sure?’, :method => :delete %> </h2>
Это упростит поиск HTML в JavaScript и применит редакторы к каждому заголовку.
Добавим редактирование через JavaScript
Последний кусочек мозаики – код JavaScript для редактирования на месте. Прежде чем писать его, поправим HTML-код основного файла и подключим туда наши скрипты. В Rails для этого есть вспомогательный класс javascript_include_tag – добавьте его в секцию <head> файла app/views/layouts/application.html.erb:
<%= javascript_include_tag :all %>
Добавьте следующий код в файл public/javascripts/application.js:
var EditorGenerator = Class.create({ initialize: function(class_name, url_base, error_message) { this.item_selector = ‘.’ + class_name this.update_url = ‘/’ + url_base + ‘/update/’ this.error_message = error_message this.field = class_name.replace(/in_place_/, ‘’) this.create_editors() }, create_editors: function() { $$(this.item_selector).each(function(element) { var item_id = element.id.match(/_(\d+)$/)[1] this.create_editor(element, item_id) }.bind(this)) }, create_editor: function(element, item_id) { new Ajax.InPlaceEditor(element, this.update_url + item_id + ‘?field=’ + this.field, { onFailure: this.onFailure.bind(this), callback: function(form, value) { return ‘photo[‘ + this.field + ‘]=’ + escape(value) }.bind(this) }) }, onFailure: function(editor, transport) { alert(this.error_message + ‘: ‘ + transport.responseText) } }) document.observe(‘dom:loaded’, function() { var title_generator = new EditorGenerator(‘in_place_title’, ‘photos’, ‘Error saving photo’) })
Этот код создает класс JavaScript, применяющий редактор Ajax.InPlaceEditor к заданным HTML-элементам. Функционал, который здесь используется, предоставляется библиотеками JavaScript Scriptaculous и Prototype – они у нас есть. Класс инициализируется вызовом document.observe(‘dom:loaded’, ...), который ждет готовности браузера, прежде чем выполнить какой-либо JavaScript. Как только класс EditorGenerator будет создан, он добавит Ajax.InPlaceEditor в каждый из span’ов, которые сгенерировал наш вспомогательный класс. Другая интересная строка кода – это итератор, используемый в цикле для прохода по каждому заголовку:
$$(this.item_selector).each(function(element) { ...
В нем используется функция Prototype $$() для выбора списка элементов на основе селектора CSS. Он хранится в свойстве this.item_selector, устанавливаемом при инициализации класса. Функция $$() возвращает массив элементов, и, значит, для прохода по нему можно использовать метод each(), также предоставляемый Prototype. Это итератор, и он считается лучше читаемым по сравнению с обычным циклом.
Для отладки JavaScript или даже исследования объектной модели документа DOM или CSS страницы, настоятельно рекомендую модуль расширения Firefox под названием Firebug (https://addons.mozilla.org/en-US/firefox/addon/1843). Без него причины ошибок в JavaScript будет понять очень трудно.
Теперь можно добавить редактируемые поля всюду, где отображаются заголовки. В качестве примера откроем файл app/views/photos/
show.html.erb и изменим его заголовок на этот:
<h2><% in_place_editor_if current_user, @photo, :title do %><%= h @photo.title %><% end %></h2>
Другой хороший кандидат на редактирование – описания. Измените их в app/views/photos/show.html.erb таким образом:
<p><% in_place_editor_if current_user, @photo, :description do %><%= textilize_without_paragraph h(@photo.description) %><% end %></p>
Потом замените описание в файле app/views/photos/_photo.rhtml на следующее:
<p><% in_place_editor_if current_user, photo, :description do %><%= textilize_without_paragraph h(photo.description) %><% end %></p>
Затем добавьте еще один объект EditorGenerator в public/javascripts/application.js, после title_generator:
var description_generator = new EditorGenerator(‘in_place_description’, ‘photos’, ‘Error saving photo’)
То же самое можно сделать с любым другим полем. Есть лишь небольшая загвоздка: если кликнуть по ссылке для редактирования описания, текст будет представлен в виде HTML-кода. Это можно поправить, добавив в Ajax.InPlaceEditor параметр loadingText, указывающий на действие в PhotosController, которое удалит ненужную разметку.
Часть 3 Разбивка на страницы
Чтобы достойно завершить наш второй урок по разработке галереи, добавим функцию постраничного вывода. Как ни странно, решение этой задачи в web-приложениях сопряжено с трудностями, так как случайно написать увесистый SQL-запрос очень просто. К счастью, Мислав Марохнич [Mislav Marohnic] создал невероятно простой модуль расширения will_paginage, удовлетворяющий требованиям большинства приложений.
Предпочтительный метод установки – с помощью gem:
sudo gem1.8 install mislav-will_paginate --source http://gems.github.com/
Теперь откройте файл config/environment.rb и добавьте в него следующие строки в конце:
gem ‘mislav-will_paginate’, ‘~> 2.2’ require ‘will_paginate’
Перезапустите сервер, чтобы приложение загрузило модуль.
Пользоваться им очень просто. Откройте app/controllers/photos_controller.rb и измените индексирование при загрузке фотографий таким образом:
@photos = Photo.paginate :all, :conditions => ‘thumbnail is NULL’, :page => params[:page], :order => ‘created_at DESC’
Теперь откройте файл app/views/photos/index.html.erb и добавьте туда строку:
<%= will_paginate @photos %>
Все почти готово, но чтобы разбивка на страницы хорошо читалась, нужно немного приятного CSS. Добавьте следующий код в файл public/stylesheets/screen.css:
.pagination { text-align: center; padding: .3em; clear: both; margintop: 1em; width: 100%; float: left; } .pagination a, .pagination span { padding: .2em .5em; } .pagination span.disabled { color: #AAA; } .pagination span.current { font-weight: bold; color: #FF0084; } .pagination a { border: 1px solid #DDDDDD; color: #0063DC; textdecoration: none; } .pagination a:hover, .pagination a:focus { border-color: #003366; background: #0063DC; color: white; } .pagination .page_info { color: #aaa; padding-top: .8em; } .pagination .prev_page, .pagination .next_page { border-width: 2px; } .pagination .prev_page { margin-right: 1em; } .pagination .next_page { margin-left: 1em; }
Пока вы не добавите не менее 30 фотографий, номеров страниц вам не видать. Для тестовых целей добавьте опцию per_page в действие index:
@photos = Photo.paginate :all, :conditions => ‘thumbnail is NULL’, :page => params[:page], :per_page => 5 :order => ‘created_at DESC’
Итак, мы изучили основы модульных тестов, разбивки на страницы, Ajax и объектно-ориентированный JavaScript; можете применить эти технологии в своих проектах. Библиотеки JavaScript Scriptaculous и Prototype имеют стабильное API, поэтому у вас будет возможность взять пример редактирования на месте и создать на его основе более сложные элементы управления. Модульное тестирование Rails поможет убедиться в работоспособности вашего кода. Вы почти полностью вооружены для профессиональной разработки на Rails! LXF
Комплект библиотек JavaScript
Rails поставляется с библиотеками Prototype и Scriptaculous. Scriptaculous (http://script.aculo.us) построена на Prototype (http://prototypejs.org). На сайтах обеих библиотек есть подробная документация. Prototype упрощает использование JavaScript на различных платформах, а также включает средства объектно-ориентированной разработки, библиотеку Ajax и абстракцию DOM для упрощения навигации в HTML-документах из ваших программ. Scriptaculous предоставляет средства для анимации, эффектов, drag and drop, сложные элементы управления Ajax и модульное тестирование.
Вспомогательные классы Rails: Быстрая справка
textilize text | Конвертирует текст в HTML с помощью Textile (установите gem-пакет RedCloth, чтобы получить эту функциональность). |
---|---|
truncate text, length | Если текст длиннее, чем length, он будет обрезан и дополнен ‘...’. |
tag name | Создает HTML-тэг, самозакрывающийся, если это необходимо. |
content_tag name, content | Создает блок с тэгом HTML, например:content_tag :p, “Example” |
number_to_human_size size | Преобразует размер файла в байтах в читаемый формат, при необходимости добавляя KB, MB, GB. |