Журнал LinuxFormat - перейти на главную

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.
Персональные инструменты
купить
подписаться
Яндекс.Метрика