Rspec and Devise reset password

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

В общем, вот, собственно, весь тест:

describe 'test reset password' do
  it "should have reset password" do
    ActionMailer::Base.deliveries.clear
    @user = FactoryGirl.build(:user, :email=>"none@ited.com.ua")

    @user.send_reset_password_instructions()

    @mail = ActionMailer::Base.deliveries.last
    @host = ActionMailer::Base.default_url_options[:host]

    @mail.should deliver_to(@user.email)
    @mail.should deliver_from(Devise.mailer_sender)
    @mail.should have_subject(/Reset password instructions/)
    @mail.should have_body_text(/#{@user.email}/)
    @reset_url_regexp = %r{<a href=\"http://#{@host}/edit_password}
    @mail.should have_body_text(/#{@reset_url_regexp}/)


    change_password_link = @mail.body.raw_source[/(http:\/\/.+\")/][0..-2]
    visit change_password_link
    page.should have_content "
Новый пароль"

    fill_in "
user[password]", :with=> 'newpassword123'
    fill_in "
user[password_confirmation]", :with=> 'newpassword123'
    click_button "
Изменить"

    page.should have_content("
Dashboard")
  end

end

Я не знаю, почему плагин так разрисовал код… Но у меня он именно такой.

Распишу, что же тут и как…

ActionMailer::Base.deliveries.clear — тут очищаем очередь писем.

@user.send_reset_password_instructions() — отсылаем письмо только что созданному пользователю с инструкциями, как восстановить пароль.

@mail = ActionMailer::Base.deliveries.last — получаем последнее отосланное письмо

@host = ActionMailer::Base.default_url_options[:host] — откуда оно было отослано

@mail.should deliver_to(@user.email) — в письме должен быть корректный адрес получателя: пользователя, который был создан.

@mail.should deliver_from(Devise.mailer_sender) — отправить должен быть наш Devise, как установлено в настройках.

@mail.should have_subject(/Reset password instructions/) — тема письма тоже должна быть стандартной, если мы сами ничего не меняли.

@mail.should have_body_text(/#{@user.email}/) — в теле письма опять же должен упоминаться пользователь, вернее, его электронная почта, для которого восстанавливается пароль.

@reset_url_regexp = %r{&lt; a href=\"http://#{@host}/edit_password}

- небольшая регулярка для проверки, что в теле письма есть ссылка на восстановление пароля

@mail.should have_body_text(/#{@reset_url_regexp}/)

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

change_password_link = @mail.body.raw_source[/(http:\/\/.+\")/][0..-2]

- получаем ссылку из тела письма

visit change_password_link — переходим по этой ссылке

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

 
Не знаю, почему плагин так изуродовал код…..

Октябрь 9th, 2012 by none | Комментариев нет

Backbone.js: Views. Часть первая.

Сегодня затронем наиболее  обширную тему: отображения(views). Думаю, что будет две части, так как тема всё-таки достаточно большая.

Итак, начнём. Для начала разберёмся с терминологией. В backbone.js отображения (views) — это совсем не то же самое, что в «рельсах». Если Вы помните, то в «рельсах» views представляют из себя просто шаблоны, в которые вставляются некоторые данные. Иногда в них присутствует некоторая простейшая логика, позволяющая отображать список однотипных элементов и т.п. В backbone.js же views больше похожи на контроллеры в «рельсах»: они оперируют данными и ответственные за то, какие данные в каком шаблоне будут отображаться. Шаблоны тоже присутствуют, но они никак отдельно не выделены.

Views будем изучать на примере пользователей — users. И начнём, как бы странно это не казалось, с роутеров. Почему? Да потому что там мы впервые сталкиваемся с вызовом классов views. Но и это не самое начало. Если помните, то в части про роутеры, я рассказывал ,что в момент инициализации нашего приложения, создаются две коллекции: для постов и для пользователей. На данный момент нас будет интересовать коллекция пользователей. Вот строчки из конструктора App.Routers.Posts:

    @users = new App.Collections.Users()
    @users.reset($('#all_users_data').data('users'))

В первой строке мы создаём экземпляр класса коллекции для пользователей, а во второй вызываем метод reset(), который загружает данные в нашу коллекцию. Об этом я уже говорил раньше. Таким образом, на момент полной загрузки странички с нашим приложением, у нас есть вся необходимая информация для работы с пользователями.

Теперь представим, что пользователь кликает по ссылке или в адресной строке набирает путь «/#users«. В соответствии с нашими роутерами, управление передаётся функции users() из всё того же класса App.Routers.Posts:

  users: ->
    $('.current').removeClass('current')
    $('#all_users').addClass('current')
    view = new App.Views.UsersIndex(collection: @users)
    @showView('#content', view)

Первые две строчки не представляют интереса — это всего лишь украшательства. А вот последние две строчки как раз и отвечают за то, чтобы мы увидели правильную информацию. В 3-ей строке мы создаём экземпляр класса App.Views.UsersIndex, который и будет отвечать за отображение пользователей в приложении. Настоятельно обращаю Ваше внимание, что ему в качестве аргумента передаётся наша коллекция пользователей (@users), которую, собственно, нам и предстоит отобразить. Рассмотрим теперь, собственно сам класс App.Views.UsersIndex:

class App.Views.UsersIndex extends Backbone.View

  template: JST['users/index']

  initialize: ->
    @collection.on('reset', @render, @)

  render: ->
    $(@el).html(@template())
    @collection.each(@appendUser)
    @

  appendUser: (user) =>
    view = new App.Views.UserItem(model: user, true)
    @$('#userslist').prepend(view.render().el)

Как видим, данный класс наследуется от Backbone.View — это и не удивительно, т.к. мы и хотим создать отображение. (-:  В следующей строке задаётся функция, которая указывает, где и какой шаблон использовать для построения html-разметки.  В данном случае мы будем использовать Javascript Template(JST). Но и это ещё не всё, что касается шаблонизатора. Лично я буду использовать Eco Template Engine, который позволяет легко и просто делать шаблоны с применением синтаксиса, похожего на erb. Если Вы посмотрите на файлы в директории /templates, то там все файлы с расширением *.jst.eco — это невероятно удобно (-: Кстати, вот содержимое самого файла /templates/users/index.jst.eco:

<div style="width: 85%; float: left; margin-bottom: 20px;">
  User
</div>
<div style="margin-bottom: 20px;">
  Action
</div>
<div class='usersList'>
  <ul id="userslist"></ul>
</div>

<div class="form-actions">
  <a href="#users/new" class="btn btn-mini">Add User</a>
</div>

В данном файле просто задётся разметка, для дальнейшего отображения пользователей.

Но вернёмся к нашему классу  отображения. Дальше следует конструктор нашего класса:

  initialize: ->
    @collection.on('reset', @render, @)

Тут у нас всего одна единственная строчка, которая выполняет очень важную функцию: привязывает функцию-обработчик @render() к событию reset для коллекции @collection, которая будет вызываться каждый раз, когда сработает событие reset. В переводе на русский язык, когда в нашу коллекцию будут загружены данные(reset), их необходимо будет отобразить(@render()). @collection — это та коллекция, которая нам была передана в момент создания экземпляра данного класса, в нашем случае это users.

Рассмотрим теперь саму функцию render().

render: ->
    $(@el).html(@template())
    @collection.each(@appendUser)
    @

Набор каких-то мало понятных символов, не так ли? (-: На самом деле всё достаточно просто. Но обо всём по-порядку.

el — это DOM-элемент нашего документа, куда будет вставлено наше отображение.

@el указывает на то, что это внешняя переменная, что она нам была передана на момент вызова нашей функции.

$(@el) — это просто jQuery способ выбрать соответствующий DOM-элемент в разметке страницы.

А теперь вся эта строка:
$(@el).html(@template()) — заменить содержимое элемента el нашим шаблоном. Только и всего.

Следующая строка не что иное, как перебор всех экземпляров в коллекции и для каждого вызов функции appendUser().

Функция appendUser() выполняет абсолютно туже функцию, только касательно не коллекции, а отдельной записи — модели. Попробую описать, что это и для чего. Я сделал так, что каркас для коллекции строится сам по себе, а уже в этот каркас добавляются данные для каждой модели отдельно. В принципе, можно было сделать так, что уже в шаблоне пробегать по все коллекции и отрисовывать каждую модель. Но такой способ ведёт к неприятным последствиям. Представьте, например, что Вам на странице надо отобразить сотню или тысячу элементов. Не вопрос, отобразить не проблема: ну подумаешь отрисовываются все элементы, это не проблема. Проблемы начинаются, когда над эти данными начинают производить какие-то операции: добавление, удаление и т.п. Так вот при всех этих процессах придётся каждый раз прорисовывать все сто или тысячу элементов! А это уже не оправданная нагрузка. Чтобы этого не происходило, за отрисовку каждого элемента отвечает свой собственный класс, всё просто и красиво (-:

Мне остаётся только добавить, что в функции appendUser() я использую jQuery функцию prepend(), чтобы в результате недавно добавленные пользователи были выше в списке, чем давно добавленные.

Ну и последняя строка в фукции render() — это просто песня: символ @. (-: Это просто Coffeescript аналог, синоним слова this из Javascript-а. Просто приведу цитату из документации backbone.js, для чего это необходимо:

Хорошее соглашение — делать return this в конце render, чтобы иметь возможность делать цепочечные вызовы.

Так как Coffeescript намного упрощает написание кода, то нам просто достаточно написать «@», что и означает return this. Красота! (-:

Вот, в принципе, и всё для первой части про views. В следующий раз поговорим про некоторые другие аспекты отображений: добавление, удаление и редактирование пользователей, а так же клиентская и серверная валидация.

Сентябрь 18th, 2012 by none | Комментариев нет

Backbone.js: Models and Collections

Сегодня коротенько пробежимся по моделям и коллекциям. Расскажу, что это и с чем это едят.

Начнём с модели:

class App.Models.Post extends Backbone.Model

  urlRoot: '/posts'

В большинстве случаев это всё, что будет указываться при описании модели: указываем, что модель наследуется от Backbone.Model, а потом указываем путь, по которому Backbone будет слать RESTful-запросы.

Я несколько слукавил, когда сказал, что это всё, что Вам надо будет писать при описании модели (-: В более-менее серьёзном приложении необходимо будет задать ещё значения по умолчанию для модели (defaults), а так же некоторую проверку вводимых значений (validation). Всё это, в принципе, выходит за рамки ознакомительной части, однако, валидацию я добавлю чуть позже, но не в слое модели, а в слое отображения (view), при помощи плагина jquery.validation: он гораздо гибче, чем встроенный в Backbone. Ну а так же покажу, как реализовать проверку на стороне сервера и отображение ответа на клиенте.

Теперь, что касается коллекций. Опять же, пример коллекции:

class App.Collections.Posts extends Backbone.Collection

  model: App.Models.Post
  url: '/posts'

Предлагаю сразу разъяснить, что такое и чем отличаются коллекции и модели, чтобы не было путаницы. Особенное ,если Вы имели опыт работы с моделями в тех же «рельсах».  Модель — это единичная запись, а коллекция — это набор этих записей, моделей. Т.е. когда вы получаете или устанавливаете атрибуты какого-то объекта, например, имя пользователя или его электронный адрес, — это работа с моделью. А вот когда вы делаете запрос к серверу, получаете, а потом отображаете всех пользователей — это уже работа с коллекцией. Коллекция — это множество моделей. И коллекция работает с моделью, о чём и указывает свойство model: App.Models.Post. А так как коллекция работает тоже с данными на сервере, то и путь указывается такой же, что и в модели.

Вот и всё, что касается моделей и коллекций. Более тесно с ними познакомимся, когда коснёмся отображений(views), т.к. картинки строятся на основании данных, полученных с сервера, а так же при создании, изменении или удалении информации пользователем.

Не теряйтесь (-:

Сентябрь 17th, 2012 by none | Комментариев нет

Backbone.js Routers

Вчера я в общих чертах описал процесс подключения Backbone.js в приложение на «рельсах». Кто не читал, тому рекомендую сначала бегло пробежать по той статье.

Сегодня расскажу о, пожалуй, самой важной части Backbone.js: о роутерах. Почему я считаю роутеры самыми важными? Всё просто: чтобы что-то увидеть в окне браузера, пользователь сначала должен набрать URL, кликнуть ссылку или что-то в этом духе. Так вот роутеры и решают, что и в какой момент времени показывать пользователю, в зависимости от URL-а. Они являются как бы связующей частью между пользователем и серверной частью приложения. Впрочем, ничего сверхъестественного.

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

Для начала давайте рассмотрим файл app/assets/javascripts/app.js.coffee:

window.App =
  Models: {}
  Collections: {}
  Views: {}
  Routers: {}
  init: ->
    new App.Routers.Posts()
    Backbone.history.start()

$(document).ready ->
  App.init()

Обращаю внимание, что моё приложение называется App. Запуск приложения - App.init() — происходит, когда уже DOM полностью загружен. Функция init() — это своего рода конструктор, если кто-то знаком с другими языками программирования. И она выполняет всего две, но очень важные функции:

1. new App.Routers.Posts() — инициализирует наши роутеры

2. Backbone.history.start() — запускает механизм сохранения истории нашего перемещения по приложению. Вы же хотите пользоваться кнопками «Вперёд» и «Назад» своего браузера? Вот это именно для этого.

Теперь перейдём непосредственно к роутерам. Все файлы, содержащие роуты, находятся в одной директории: app/assets/javascripts/routers/. У меня там всего один файл: posts.js.coffee. В данном файле находится всего один класс App.Routers.Posts, который наследуется от Backbone.Router. Собственно, сегодня я буду рассказывать в основном про этот класс.

Начинается этот класс с перечня роутеров, которые допустимы в нашем приложении:

routes:
  ''      : 'index'
  'posts/new'   : 'add'
  'posts/:id'   : 'show'
  'posts/:id/edit'  : 'edit'
  'users'     : 'users'
  'users/new'   : 'newUser'
  'users/:id'   : 'showUser'
  'users/:id/edit'  : 'editUser'
  'help'      : 'help'

Думаю те, кто привыкли работать с роутами в «рельсах», найдут много общего. Слева указывается часть URL-а, который в адресной строке, справа — функция, которая обрабатывает данный путь. Ничего сложного и необычного.

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

  initialize: ->
    @collection = new App.Collections.Posts()
    @collection.reset($('#all_posts_data').data('posts'))

    @users = new App.Collections.Users()
    @users.reset($('#all_users_data').data('users'))

    view = new App.Views.Menu()
    $('#sidebar').html(view.render().el)

Что же делает наш конструктор?? Первые две строчки аналогичны третьей и четвёртой, разница лишь в разных данных. Сначала мы инициализируем коллекцию

@collection = new App.Collections.Posts()

а после этого загружаем в коллекцию данные

@collection.reset($('#all_posts_data').data('posts'))

Данный приём является примером хорошего тона, когда при загрузке и старте приложения не приходится делать ещё один или несколько запросов к серверу, чтобы получить данные. Кроме этого, не происходит задержки при отображении данных. Но позвольте, скажут некоторые, а откуда эти данные взялись на странице??? Ответ очень прост: стоит ознакомиться с содержимым файла app/views/main/index.html.erb. В самом конце есть две вот такие строчки:

<%= content_tag :div, '', id: "all_posts_data", data: {posts: Post.all} %>
<%= content_tag :div, '', id: "all_users_data", data: {users: User.all} %>

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

Но вернёмся к нашему конструктору класса. Остались не рассмотренными ещё две строчки:

    view = new App.Views.Menu()
    $('#sidebar').html(view.render().el)

Данные строчки выполняют всего одну функцию: отображают на экране меню с левой стороны. Только и всего.

В принципе, большинство остальных функций в данном классе выполняют одно и то же действие:  формируют необходимую картинку на экране, в зависимости от URL-а. Рассмотрим на примере метода index.

  index: ->
    $('.current').removeClass('current')
    $('#all_posts').addClass('current')
    $('.active').removeClass('active')
    $('#home_link').addClass('active')
    view = new App.Views.PostsIndex(collection: @collection)
    @showView('#content', view)

Первые 4-ре строки — это не что иное, как украшательства: осуществляют подсветку необходимого пункта меня слева и вверху. Не более.

Следующая строка view = new App.Views.PostsIndex(collection: @collection)  осуществляет создание экземпляра view класса App.Views.PostsIndex, ответственного за отображение всех постов приложения. Почему всех? Да потому что в качестве аргумента передаётся коллекция, в которую были загружены все данные, полученные с сервера. Впрочем, об отображениях (views), мы поговорим в следующих постах.

Сейчас же стоит обратить внимание на функцию showView(), которая, собственно, и осуществляет вывод на экран. Вот что представляет из себя эта функция:

  showView: (selector, view) ->
    @currentView.close()  if @currentView
    $(selector).html view.render().el
    @currentView = view
    view

Может возникнуть резонный вопрос: а почему так сложно? почему бы просто не заменить содержимое элемента новым содержимым? В принципе, так можно, и так делают. Только вот есть одно «но»: как только Ваше приложение начнёт разрастаться, когда у него появится большое количество обработок действий пользователя(events), от тогда начнут возникать всякие неожиданные результаты — нажимаете одну кнопку, а действие происходит для других данных, появляются ошибки и всё такое. Чтобы этого не происходило, необходимо текущее отображение корректно закрыть, прекратить (unbind) действие всех ранее объявленных событий(events), а вот на его место уже отобразить новые данные.

Закрытие текущего представления(view) происходит при помощи функции close(), объявленной в самом начале файла:

Backbone.View::close = ->
  @beforeClose()  if @beforeClose
  @remove()
  @unbind()

О действии функций remove() и unbind() рекомендую ознакомиться на сайте jQuery.

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

В следующий раз поговорим о моделях и коллекциях.

Сентябрь 14th, 2012 by none | Комментариев нет

Backbone.js + Coffeescript + Rails

Не так давно я написал статью о создании SPA на основе Spine.js в качестве front-end-а и «рельсов» в качестве back-end-а. Spine.js очень интересная штука, очень простая и понятная, но для создания не очень сложного приложения. Как только приложение начинает разрастаться, начинаются проблемы. Как по мне, то главная проблема — это роуты, разбросанные по разным файлам, малопонятный substack. Остальное уже как-то по мелочи.

Поигравшись со Spine.js, я переключил свой взор на более обкатанную и несколько более распространённую библиотеку: Backbone.js. Как оказалось, он не на много сложнее Spine.js. Основное неудобство было в том, что все туториалы написаны для javascript-a, а мне уже давно как-то по душе Coffeescript. Именно поэтому я решил написать несколько постов о том, как их подружить друг с другом: Backbone.js, CoffeeScript, Twitter Bootstrap и «рельсы».

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

Данная статья будет вводная, много кода в ней не будет. Все остальные программные изыскания будут несколько позже. На странице работающего примера в разделе «Resources» можно найти несколько полезных ссылок, которые помогут Вам заполнить некоторые пробелы в знаниях по данной теме.

Итак, начнём. Просто создайте новое, пустое приложение «рельсов»:

$ rails new backbone_app --skip-bundle
$ cd backbone_app

После этого в Gemfile добавляем следующее:

1. вместо gem ‘sqlite3′ :

group :development do
    gem 'sqlite3'
end

group :production do
    gem 'pg'
end

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

2. в группу :assets добавляем

gem 'twitter-bootstrap-rails'

3. а в конец файла добавляем самый важный gem для этого приложения:

gem 'backbone-on-rails'

После всех этих добавлений можно смело установить все необходимые gem-ы:

$ bundle install

Теперь создадим каркас нашего приложения для работы с Backbone.js. Для этого, в строгом соответствии с документацией, выполним следующую команду:

$ rails generate backbone:install

По умолчанию, генератор создаст файлы с расширением *.js.coffee, для использования синтаксиса CoffeeScript. Если Вам более привычно и удобно работать с javascript-ом, то укажите от этом генератору, задав опцию --javascript

Данный генератор создаст несколько директорий в /app:

app/assets/
├── javascripts
│ ├── application.js
│ ├── backbone_app.js.coffee
│ ├── collections
│ ├── models
│ ├── routers
│ └── views
└── templates

Лично я сразу переименовываю файлы, типа backbone_app.js.coffee, на app.js.coffee. Мне так удобнее. Хотя… лукавлю (-: Это у меня после прочтения вот этой статьи. Кстати, тоже рекомендуется к прочтению.

Рекомендую ознакомиться с содержимым файлов application.js и app.js.coffee после выполнения предыдущей команды.

В принципе, наше приложение готово для дальнейшей работы с Backbone.js. В последующих частях я расскажу у роутерах, моделях, коллекциях и отображениях(views). Как, что и с чем едят.

Всем удачи и работающего кода! (-:

Сентябрь 13th, 2012 by none | Комментариев нет