Главу 5 мы закончили созданием страницы-заглушки для регистрации пользователей (Раздел 5.4); в течение следующих четырех глав мы выполним обещание, неявное в этой начинающейся странице регистрации. Первый важный шаг это создание модели данных для пользователей нашего сайта, вместе со способом хранить эти данные. В Главе 7 мы дадим пользователям возможность регистрироваться на нашем сайте и создадим страницу профиля пользователя. Как только пример приложения сможет создавать новых пользователей, мы также позволим им входить и выходить (Глава 8) и в Главе 9 (Раздел 9.2.1) мы узнаем как защитить страницы от несанкцонированного доступа. Взятые вместе, материалы с Главы 6 по Главу 9 разрабатывают полную Rails систему входа и аутентификации. Как вы, возможно, знаете, для Rails существует множество готовых решений для аутентификации; Блок 6.1 поясняет почему разворачивание собственной системы является лучшей идеей.
Это длинная и насыщенная действиями глава и вам она может показаться необычайно сложной, особенно если вы новичок в моделировании данных. Тем не менее, по ее окончании мы создадим весьма качественную систему для валидации, хранения и извлечения информации о пользователе.
___________________________________________
Блок 6.1.Прикручивание собственной системы аутентификации
Фактически всем веб-приложениям в настоящее время требуется какая либо система входа и аутентификации. Неудивительно, что у большинства веб-фреймворков есть множество вариантов реализации подобных систем, и Rails не исключение. Примеры систем аутентификации и авторизации включают в себя Clearance, Authlogic, Devise и CanCan (так же как не-Rails-специфичные решения, построенные на основе OpenID или OAuth). Резонный вопрос - почему мы должны изобретать велосипед. Почему бы просто не использовать готовое решение вместо того чтобы прикручивать свое?
С одной стороны, практика показывает, что аутентификация на множестве сайтов требует серьезной кастомизации и модификация стороннего продукта, это, обычно, даже большая работа чем написание собственной системы с нуля. К тому же, готовые решения это "черные ящики", с весьма загадочными внутренностями; а когда вы пишете свою собственную систему у вас гораздо больше шансов разобраться в ней. Кроме того, последние дополнения к Rails (Раздел 6.3) очень облегчили написание собственной системы аутентификации. Наконец, если вы все же когда-либо решите использовать стороннюю систему, вам будет гораздо проще в ней разобраться если вы прежде имели опыт написания собственной .
___________________________________________
(Первая строка здесь только для того чтобы удостовериться, что вы находитесь на master ветке, чтобы тема ветки modeling-users была основана на master ветке. Можно пропустить эту команду, если вы уже находитесь в master ветке.)
Хотя конечная цель следующих трех глав это создание страницы регистрации для нашего сайта (ее набросок показан на рис 6.1), в принятии регистрационной информации сейчас хорошего мало, так как нам в настоящий момент попросту негде ее хранить. Таким образом, первый шаг в регистрации пользователей должен создать структуру данных для получения и хранения их информации.
(рис 6.1) Набросок страницы регистрации пользователей.
В Rails дефолтную структуру данных для модели данных называют, что достаточно естественно, модель (М. в MVC из Раздела 1.2.6). Дефолтное решение Rails для проблемы персистентности состоит в том, чтобы использовать базу данных для долгосрочного хранения данных и дефолтную библиотеку Active Record для взаимодействия с базой данных.
Можно вспомнить из Раздела 4.4.5 , что мы уже встречали, в созданном нами классе User объекты user с атрибутами name и email. Тот класс служил полезным примером, но он испытывал недостаток в критическом свойстве персистентности: когда мы создали объект User в консоли Rails, он исчез, как только мы вышли. Наша цель в этом Разделе состоит в том, чтобы создать модель для пользователей, которые не будут исчезать так легко.
Как и с классом User в Разделе 4.4.5, мы начнем с моделирования пользователя с двумя атрибутами:
class User attr_accessor :name, :email . . . end
Напротив, при использовании Rails, для моделирования пользователей мы не должны идентифицировать атрибуты явно. Как было кратко отмечено выше, для хранения данных Rails по умолчанию использует реляционные базы данных, которые состоят из таблиц составленных из строк, данных, где у каждой строки есть столбцы атрибутов данных. Например, для того, чтобы сохранить пользователей с именами и адресами электронной почты, мы составим таблицу users со столбцами name и email (с каждой строкой, соответствующей одному пользователю). Называя столбцы таким образом, мы позволяем Active Record выводить атрибуты объектов User для нас.
Давайте посмотрим как это работает. (Если это обсуждение становится слишком абстрактным на ваш взгляд, будьте терпеливы; консольные примеры, начинающиеся в Разделе 6.1.3 и скриншоты браузера базы данных на рис 6.3 и рис 6.6 должны многое прояснить.) Вспомните из Листинге 5.31 что мы создавали контроллер Users (наряду с new действием) используя команду
$ rails generate controller Users new --no-test-framework
Есть аналогичная команда для создания модели: generate model. Листинге 6.1 показывает команду для генерации модели User с двумя атрибутами, name и email.
$ rails generate model User name:string email:string
invoke active_record
create db/migrate/[timestamp]_create_users.rb
create app/models/user.rb
invoke rspec
create spec/models/user_spec.rb
(Обратите внимание, что, в отличие от множественного соглашения для имен контроллеров, названия моделей - в ед. числе: контроллер Users, но модель User.) Передавая дополнительные параметры name:string и email:string, мы говорим Rails о двух желаемых атрибутах, наряду с тем, какого типа эти атрибуты должны быть (в данном случае, string). Сравните это с включением имен действий в Листинге 3.4 и Листинге 5.31.
Одним из результатов generate команды в Листинге 6.1 является новый файл, названный migration. Миграции обеспечивают возможность постепенного изменения структуры базы данных, так, чтобы наша модель данных могла адаптироваться к изменяющимся требованиям. В случае модели User, миграция создается автоматически сценарием генерации модели; что создает таблицу users с двумя столбцами, name и email, как это показано в Листинге 6.2. (Мы увидим в Разделе 6.2.5 и еще раз в Разделе 6.3 как создавать миграцию с нуля.)
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
Обратите внимание: у названия файла миграции есть префикс в виде временнОй отметки основанной на времени генерации миграции. В первые дни миграций, названия файлов имели префиксы в виде увеличивающихся целых чисел, что приводило к конфликтам в командах разработчиков в случаях когда несколько программистов создавали миграции с совпадающими номерами. Использование временнЫх меток позволило комфортно избегать подобных коллизий.
Сама миграция представляет собой метод
(рис 6.2) Модель данных 'пользователи', произведенная Листингом 6.2.
Мы можем запустить миграцию, известную как "migrating up", используя rake команду (Блок 2.1) следующим образом:
$ bundle exec rake db:migrate
(Можно вспомнить, что мы запускали эту команду прежде, в Разделе 2.2.) При первом запуске ); сравните со схемой на рис 6.2. Вы могли отметить, что есть один столбец в рис 6.3 неучтенный в миграции: столбец id. Как было вкратце отмечено в Разделе 2.2, этот столбец создается автоматически, и используется Rails в качестве уникального идентификатора каждой строки.
(рис 6.3) SQLite Database Browser с нашей новой users таблицей.
Большинство миграций являются обратимыми, а это означает что мы можем "migrate down" и переделать ее с помощью единственной Rake задачи, называемой db:rollback:
$ bundle exec rake db:rollback
(См. в Блоке 3.2 еще одну полезную для обращения миграций технику.) Под капотом этой Rake задачи происходит выполнение команды drop_table для удаления таблицы users из базы данных. Причина по которой это работает кроется в том, что метод change знает что drop_table это команда обратная create_table и это означает что способ отката миграции легко определим. В случае необратимых миграций, таких как удаление столбца из базы данных, необходимо определять отдельные up и down методы вместо единственного метода change. Почитайте о миграциях в (rus)Rails Guides дабы составить о них более полное представление.
Если вы откатывали базу данных, migrate up снова перед продолжением:
$ bundle exec rake db:migrate
Мы видели, как генерация модели User в Листинге 6.1 сгенерировала файл миграции (Листинг 6.2) и мы видели на рис 6.1" type="example">Листинге 6.1 сгенерировала файл миграции (Листинг 6.2) и мы видели на users со столбцами id, name, email, created_at и updated_at. Листинг 6.1 также создал саму модель; остальная часть этого раздела посвящена ее изучению.
Мы начнем с рассмотрения кода для модели User, которая живет в файле user.rb в каталоге app/models/ это, мягко выражаясь, очень компактно (Листинг 6.3).
class User < ActiveRecord::Base end
Вспомните из Раздела 4.4.2 что синтаксис class User < ActiveRecord::Base означает что класс User наследует от ActiveRecord::Base, таким образом, у модели User автоматически есть вся функциональность ActiveRecord::Base класса. Конечно, знание этого наследования не приносит пользы, если мы не знаем что содержит ActiveRecord::Base так что давайте начнем с конкретных примеров.
Как и в Главе 4, наш инструмент - консоль Rails. Так как мы (пока) не хотим производить какие либо изменения в нашей базе данных, мы запустим консоль в
$ rails console --sandbox Loading development environment in sandbox Any modifications you make will be rolled back on exit >>
Как обозначено полезным сообщением "Любые модификации которые вы сделаете откатятся при выходе", при работе в песочнице, консоль будет "откатывать" (то есть, отменять) любые изменения базы данных, созданные во время сеанса.
В консольной сессии в Разделе 4.4.5 мы создавали нового пользователя с User.new, к которому мы имели доступ только после подгрузки файла example user из Листинга 4.9. С моделями ситуация иная; как вы можете вспомнить из Раздела 4.4.4, Rails консоль автоматически загружает окружение Rails, которое включает модели. Это означает что мы можем создавать новые объекты user без необходимости подгружать что либо:
>> User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
Мы видим здесь дефолтное представление объекта user.
Вызванный без параметров, User.new возвращает объект с nil атрибутами. В Разделе 4.4.5 мы спроектировали пример класса User таким образом, чтобы он принимал инициализационный хэш для установки атрибутов объекта; такое решение было обусловлено библиотекой Active Record, которая позволяет инициализировать объекты тем же способом:
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com") => #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil>
Здесь мы видим, что, как и ожидалось, атрибуты имени и адреса электронной почты были установлены.
Если вы следили за development log, вы, возможно, заметили, что новые строки еще не обнаружились. Это связано с тем, что вызов User.new не касается базы данных; он просто создает новый Ruby объект в памяти. Чтобы сохранить объект user в базе данных, мы вызовем метод save на переменной user:
>> user.save => true
Метод save возвращает true если сохранение успешно выполнилось и false если сохранение не выполнено. (Сейчас все сохранения должны успешно выполняться; но в Разделе 6.2 мы увидим случаи, когда некоторые из них не сработают.) После сохранения в логе консоли должна появиться строка с командой SQL INSERT INTO "users". Из-за множества методов, предоставляемых Active Record, в этой книге нам не потребуется необработанный SQL и я буду опускать обсуждение команд SQL с этого момента. Но вы можете многому научиться, читая SQL соответствующий командам Active Record.
Вы, возможно, заметили что у нового объекта user были nil значения для атрибутов id и волшебных столбцов created_at и updated_at. Давайте посмотрим, изменило ли наше save что-нибудь:
>> user => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">
Мы видим что 2013-03-11 00:57:46" вызвало ваше любопытство - я не пишу это после полуночи; временнЫе метки записаны во Всемирном координированном времени (UTC), которое для многих практических целей является аналогом Среднего времени по Гринвичу. Из NIST Time and Frequency FAQ: Q: Почему UTC используется в качестве акронима к Coordinated Universal Time вместо CUT? A: В 1970 система Coordinated Universal Time была разработана международной консультативной группой технических экспертов в рамках International Telecommunication Union (ITU). ITU чувствовал, что было лучше определить единственное сокращение для использования на всех языках, чтобы минимизировать беспорядок. Так как единогласное соглашение не могло быть достигнуто при использовании английского порядка слов, CUT, или французского порядка слов, TUC, акроним, UTC был
выбран в качестве компромисса.
Как и с классом User в Разделе 4.4.5, экземпляры модели User предоставляют доступ к своим атрибутам, используя точку:user.updated_at говорит вам о том что временнАя метка была в UTC
>> user.name => "Michael Hartl" >> user.email => "mhartl@example.com" >> user.updated_at => Mon, 11 Mar 2013 00:57:46 UTC +00:00
Как мы увидим в Главе 7, часто бывает удобно создать и сохранить модель в два приема, как мы это сделали выше, но Active Record также позволяет вам объединить эти действия в один шаг с User.create:
>> User.create(name: "A Nother", email: "another@example.org") #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2013-03-11 01:05:24", updated_at: "2013-03-11 01:05:24"> >> foo = User.create(name: "Foo", email: "foo@bar.com") #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2013-03-11 01:05:42", updated_at: "2013-03-11 01:05:42">
Обратите внимание: User.create, вместо того чтобы возвратить true или false, возвращает сам объект User который мы можем дополнительно присвоить переменной (такой как foo во второй команде выше).
Команда, обратная create это destroy:
>> foo.destroy => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2013-03-11 01:05:42", updated_at: "2013-03-11 01:05:42">
Странно, destroy, как и create, возвращает рассматриваемый объект, хотя я не могу вспомнить что когда-либо использовал значение, возвращаемое destroy. Еще более странно то, что destroyенный объект все еще существует в памяти:
>> foo => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2013-03-11 01:05:42", updated_at: "2013-03-11 01:05:42">
Как мы узнаем, уничтожили ли мы в действительности объект? И как мы можем получить сохраненные и неуничтоженные объекты user из базы данных? Пора узнать, как использовать Active Record, для поиска объектов user.
Active Record предоставляет несколько способов поиска объектов. Давайте используем их, для того, чтобы найти первого пользователя, которого мы создали, и чтобы проверить, что третий пользователь (foo) был уничтожен. Мы начнем с существующего пользователя:
>> User.find(1) => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">
Здесь мы передали id пользователя в User.find; Active Record вернула пользователя с этим атрибутом id.
Давайте посмотрим, существует ли пользователь с id 3 в базе данных:
>> User.find(3) ActiveRecord::RecordNotFound: Couldn't find User with ID=3
Так как мы уничтожили нашего третьего пользователя в Разделе 6.1.3, Active Record не может найти его в базе данных. Вместо этого
В дополнение к универсальному find, Active Record также позволяет нам искать пользователей по определенным атрибутами:
>> User.find_by_email("mhartl@example.com")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">
Метод find_by_email автоматически создается библиотекой Active Record на основе email атрибута в таблице users. (Как вы догадываетесь, Active Record также создает метод find_by_name.) Начиная с Rails 4.0, более предпочтительным способом для поиска атрибута является использование метода find_by с атрибутом передаваемым в виде хэша:
>> User.find_by(email: "mhartl@example.com") => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">
Поскольку мы будем использовать адреса электронной почты в качестве имен пользователей, этот вид find будет полезен когда мы узнаем как позволить пользователям регистрироваться на нашем сайте (Глава 7). Если вы беспокоитесь об эффективности find_by при большом количестве пользователей - ваше беспокойство вполне обоснованно, но вы немного забегаете вперед; мы обсудим эту проблему и ее решение в Разделе 6.2.5.
Мы закончим несколькими более общими способами поиска пользователей. Во-первых, first:
>> User.first => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">
стественно, first просто возвращает первого пользователя в базе данных. Есть также all:
>> User.all => [#<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">, #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2013-03-11 01:05:24", updated_at: "2013-03-11 01:05:24">]
Не сюрприз, что all возвращает массив (Раздел 4.3.1) всех пользователей в базе данных.
После создания объектов мы зачастую хотим их обновить. Есть два основных способа сделать это. Во-первых, мы можем присвоить атрибуты индивидуально, как мы это делали в Разделе 4.4.5:
>> user # Просто напоминание о наших атрибутах пользователя => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46"> >> user.email = "mhartl@example.net" => "mhartl@example.net" >> user.save => true
Обратите внимание: заключительный шаг необходим, для того чтобы записать изменения в базу данных. Мы можем увидеть, что произойдет без сохранения, используя reload, которая перезагружает объекты, опираясь на информацию в базе данных:
>> user.email => "mhartl@example.net" >> user.email = "foo@bar.com" => "foo@bar.com" >> user.reload.email => "mhartl@example.net"
Теперь, когда мы обновили пользователя, выполнив user.save, волшебные столбцы отличаются, как и обещалось в Разделе 6.1.3:
>> user.created_at => "2013-03-11 00:57:46" >> user.updated_at => "2013-03-11 01:37:32"
Второй основной способ обновления атрибутов заключается в использовании update_attributes:
>> user.update_attributes(name: "The Dude", email: "dude@abides.org") => true >> user.name => "The Dude" >> user.email => "dude@abides.org"
Update_attributes метод принимает хеш атрибутов и в случае успеха выполняет и обновление, и сохранение за один шаг (возвращающая true чтобы указать что сохранение произошло). Обратите внимание - если какая-нибудь из валидаций выдаст ошибку, что может случиться если, например, пароль был указан как обязательный для сохранения записи (как это реализовано в Разделе 6.3), вызов update_attributes провалится и обновления записи не произойдет. Если мы хотим обновить только один атрибут, то использование сингулярного update_attribute позволит обойти это ограничение:
>> user.update_attribute(:name, "The Dude") => true >> user.name => "The Dude"
У модели User, которую мы создали в Разделе 6.1 теперь есть рабочие атрибуты name и email, но они абсолютно универсальны: любая строка (включая пустую) в настоящий момент допустима. И все же, имена и адреса электронной почты это нечто более определенное. Например, name не должно быть пробелом, email должен соответствовать определенному формату, характерному для адресов электронной почты. Кроме того, так как мы будем использовать адреса электронной почты в качестве уникальных имен пользователей при регистрации, мы не должны позволять дублироваться адресам электронной почты в базе данных.
Короче говоря, мы не должны позволить name и email быть просто любыми строками; мы должны реализовать определенные ограничения для их значений. Active Record позволяет нам налагать такие ограничения, с помощью validations. В этом разделе мы рассмотрим несколько из наиболее распространенных случаев, применив валидации для наличия, длины, формата и уникальности. В Разделе 6.3.4 мы добавим заключительную общепринятую валидацию - подтверждение. И мы увидим в Разделе 7.3 как валидации дают нам удобные сообщения об ошибках когда пользователи предоставляют данные которые нарушают их.
Как и все прочие фичи этого примера приложения, мы добавим валидации модели User с помощью разработки через тестирование. Поскольку мы не передали флаг
--no-test-framework
при генерации модели User (в отличие, например, от Листинга 5.31), команда в Листинге 6.1 создала начальные спеки для тестирования пользователей, но в данном случае они практически пусты (Листинг 6.4).
require 'spec_helper'
describe User do
pending "add some examples to (or delete) #{__FILE__}"
end
Здесь просто используется метод pending для указания на то, что мы должны заполнить спек чем-нибудь полезным. Мы cможем увидеть результат его применения подготовив (пустую) тестовую базу данных и запустив спек модели User:
$ bundle exec rake db:migrate $ bundle exec rake test:prepare $ bundle exec rspec spec/models/user_spec.rb * Finished in 0.01999 seconds 1 example, 0 failures, 1 pending Pending: User add some examples to (or delete) /Users/mhartl/rails_projects/sample_app/spec/models/user_spec.rb (Not Yet Implemented)
На большинстве систем, ожидающие спеки отображаются желтыми, для указания на то что они находятся как бы между проходящими (зеленый) и провальными (красный) тестами.
Это наша первая встреча с командой которая создает тестовую базу данных с корректной структурой:
$ bundle exec rake test:prepare
Это просто обеспечивает соответствие между моделью данных базы данных для разработки в rake test:prepare - возможно это решит проблему.)
Мы последуем совету дефолтного спека и заполним его небольшим количеством RSpec примеров, как это показано в Листинге 6.5.
require 'spec_helper'
describe User do
before { @user = User.new(name: "Example User", email: "user@example.com") }
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
end
Блок before, который мы видели в Листинге 5.30), запускает код внутри блока перед каждым тестом, в данном случае, создавая новую переменную экземпляра @user с помощью User.new и валидного инициализационного хэша. Затем
subject { @user }
делает @user дефолтным cубъектом тестирования, как мы это видели прежде в контексте переменной page в Разделе 5.3.4.
Два теста в Листинге 6.5 тестируют на наличие name и email атрибутов:
it { should respond_to(:name) }
it { should respond_to(:email) }
Сами по себе эти тесты не особенно полезны, так как объект User у которого нет (например) атрибута name бросит исключение в блоке before. Но эти тесты проверяют что конструкции user.name и user.email являются валидными, в то время как блок before лишь тестирует атрибуты когда они передаются в виде хэша методу new. К тому же, тестирование атрибутов модели это полезная конвенция, так как она позволяет нам с первого взгляда увидеть методы на которые должна отвечать модель.
Методы respond_to неявно используют Ruby метод respond_to? который принимает символ и возвращает true в случае если объект отвечает на данный метод или атрибут и возвращает false в противном случае:
$ rails console --sandbox >> user = User.new >> user.respond_to?(:name) => true >> user.respond_to?(:foobar) => false
(Вспомните из Раздела 4.2.3 что Ruby использует знак вопроса для обозначения таких true/false булевых методов.) Сами тесты опираются на булевую конвенцию используемую RSpec: код
@user.respond_to?(:name)
может быть протестирован с помощью такого RSpec кода
it "should respond to 'name'" do expect(@user).to respond_to(:name) end
Благодаря subject { @user }, мы можем написать это в альтернативном однострочном стиле, впервые представленном в Разделе 5.3.4:
it { should respond_to(:name) }
Такой вид тестов позволяет нам использовать TDD для добавления новых атрибутов и методов к нашей модели User и, в качестве побочного эффекта, мы получаем хорошую спецификацию методов на которые должны отвечать все объекты User.
Так как мы уже подготовили тестовую базу данных командой rake test:prepare, тесты должны пройти:
$ bundle exec rspec spec/
Возможно самой элементарной валидацией является валидация наличия, которая просто проверяет что данный атрибут имеется в наличии. Например, в данном разделе мы убеждаемся что оба name и email поля заполнены прежде чем пользователь будет сохранен в базе данных. В Разделе 7.3.3 мы увидим как распространить это требование на форму регистрации новых пользователей.
Мы начнем с теста на наличие атрибута name. Хотя первым шагом в TDD является написание провального теста (Раздел 3.2.1), в данном случае мы пока недостаточно знаем о валидации для того чтобы написать годный тест, так что мы вначале напишем валидацию и немного поиграем с ней в консоли, - чтобы побольше узнать о ней. Затем мы закомментируем валидацию, напишем провальный тест и проверим что раскомментирование валидации приводит к прохождению теста. Эта процедура может показаться излишне педантичной для такого простого теста, но я видел много "простых" тестов, которые на самом деле тестировали неправильные вещи; дотошность в TDD это просто единственный способ быть уверенными в том что мы тестируем правильные вещи. (Такая техника закомментирования также полезна при спасении приложения, код которого уже написан, но—quelle horreur! — (# какой ужас - фр.) не имеет тестов.)
Способ валидации наличия атрибута имени заключается в применении метода validates с аргументом presence: true, как это показано в Листинге 6.6. Аргумент presence: true это одноэлементный хэш опций; вспомните из Раздела 4.3.4 что фигурные скобки являются необязательными при передаче хеша в качестве последнего аргумента в методе. (Как отмечено в Разделе 5.1.1, использование хэшэй опций это очень распространенный прием в Rails.)
class User < ActiveRecord::Base validates :name, presence: true end
Листинг 6.6 возможно выглядит как магия, но validates это просто метод. Эквивалентная Листингу 6.6 формулировка с применением скобок выглядит следующим образом:
class User < ActiveRecord::Base validates :name, presence: true end
Давайте заскочим в консоль чтобы увидеть эффект добавления валидации к нашей модели User:User.new.
$ rails console --sandbox >> user = User.new(name: "", email: "mhartl@example.com") >> user.save => false >> user.valid? => false
Здесь user.save возвращает false, указывая на провальное сохранение. В заключительной команде мы используем valid? метод, который возвращает false когда объект приводит к сбою одной или более валидаций, и true когда все валидации проходят. В данном случае у нас есть только одна валидация, таким образом, мы знаем, какая именно провалилась, но все же не лишним будет в этом убедиться с помощью объекта errors, генерируемого при отказе:
>> user.errors.full_messages => ["Name can't be blank"]
(Сообщение об ошибке - подсказка, говорящая о том что Rails проверяет наличие атрибута, используя blank? метод, который мы видели в конце Раздела 4.4.3.)
Теперь о провальном тесте. Чтобы гарантировать что наш начальный тест перестанет работать, давайте закомментируем валидацию (Листинге 6.7).
class User < ActiveRecord::Base # validates :name, presence: true end
Начальный тест валидации представлен в Листинге 6.8.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should be_valid }
describe "when name is not present" do
before { @user.name = " " }
it { should_not be_valid }
end
end
Первый новый тест это просто проверка на то что объект @user изначально валиден:
it { should be_valid }
Это еще один пример булевой конвенции RSpec которую мы видели ранее в Разделе 6.2.1: в каждом случае, когда объект отвечает на булевый метод foo?, существует соответствующий тестовый метод с именем be_foo. В данном случае мы можем протестировать результат вызова
@user.valid?
с помощью
it "should be valid" do expect(@user).to be_valid end
Как и прежде, subject { @user } позволяет нам использовать однострочный стиль, что приводит к
it { should be_valid }
Второй тест вначале назначает пользовательскому имени недопустимое значение, а затем проверяет что получившийся объект @user невалиден:
describe "when name is not present" do
before { @user.name = " " }
it { should_not be_valid }
end
Здесь используется блок before для назначения невалидного значения атрибуту name, а затем происходит проверка того что получившийся объект user невалиден.
Теперь необходимо убедиться в том что в данный момент тесты провальны:
$ bundle exec rspec spec/models/user_spec.rb ...F 4 examples, 1 failure
Теперь раскомментируем валидацию (т.е., вернемся от Листинга 6.7 обратно к Листингу 6.6) для того чтобы получить прохождение теста:
$ bundle exec rspec spec/models/user_spec.rb .... 4 examples, 0 failures
Конечно, мы также хотим валидировать наличие адресов электронной почты. Тест (Листинг 6.9 ) походит на аналогичный тест для атрибута name.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when email is not present" do
before { @user.email = " " }
it { should_not be_valid }
end
end
Реализация практически та же, что и в Листинге 6.10.
class User < ActiveRecord::Base validates :name, presence: true validates :email, presence: true end
Теперь все тесты должны проходить и валидации "наличия" готовы.
Мы ограничили нашу модель User требованием имени для каждого пользователя, но мы должны пойти еще дальше: имена пользователей будут отображаться на сайте, таким образом, мы должны будем реализовать некоторое ограничение их длины. С работой, проделанной в Разделе 6.2.2, этот шаг легок.
Мы начнем с теста. В выборе максимальной длины нет ничего хитрого; мы просто примем 50 как разумную верхнюю границу, что означает что имена длиной в 51 символ будут слишком длинными (Листинг 6.11).
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when name is too long" do
before { @user.name = "a" * 51 }
it { should_not be_valid }
end
end
Для удобства мы использовали "мультипликацию строки" в Листинге 6.11 для создания строки длиной в 51 символ. Мы можем увидеть как это работает, используя консоль:
>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51
Тест в Листинге 6.11 должен провалиться. Чтобы заставить его пройти, мы должны знать об аргументе валидации, ограничивающим длину, :length, наряду с :maximum параметром реализуют верхнюю границу (Листинг 6.12).
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true
end
С нашим комплектом тестов, вновь проходящим, мы можем идти дальше, к более интересной валидации: валидации формата электронной почты.
Наши валидации для атрибута name реализуют только минимальные ограничения: любое непустое имя длиной до 51 символов пройдет; но, конечно, атрибут email должен соответствовать более строгим требованиям. До сих пор мы отклоняли только пустой адрес электронной почты; в этом разделе мы потребуем, чтобы адреса электронной почты соответствовали знакомому образцу user@example.com.
Ни тесты, ни валидации не будут исчерпывающими - лишь достаточно хорошими, чтобы принять большую часть допустимых адресов электронной почты и отклонить большинство недопустимых. Мы начнем с пары тестов, включающих наборы допустимых и недопустимых адресов. Чтобы сделать эти наборы, стоит узнать о полезной технике создания массивов строк, как показано в этом консольном сеансе:
>> %w[foo bar baz] => ["foo", "bar", "baz"] >> addresses = %w[user@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp] => ["user@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"] >> addresses.each do |address| ?> puts address >> end user@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp
Здесь мы выполнили итерации по элементам массива addresses используя each метод (Раздел 4.3.2). Вооружившись этой техникой мы готовы написать несколько базовых тестов для валидации формата электронной почты (Листинге 6.13).
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when email format is invalid" do
it "should be invalid" do
addresses = %w[user@foo,com user_at_foo.org example.user@foo.
foo@bar_baz.com foo@bar+baz.com]
addresses.each do |invalid_address|
@user.email = invalid_address
expect(@user).not_to be_valid
end
end
end
describe "when email format is valid" do
it "should be valid" do
addresses = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
addresses.each do |valid_address|
@user.email = valid_address
expect(@user).to be_valid
end
end
end
end
Как было отмечено выше, они не являются исчерпывающими, но мы проверили обычные допустимые формы электронной почты user@foo.COM, THE_US-ER@foo.bar.org (верхний регистр, подчеркивание и соединенные домены) и first.last@foo.jp (стандартное корпоративное имя пользователя first.last, с двухбуквенным доменом верхнего уровня jp (Japan)), наряду с несколькими недопустимыми формами.
Код приложения для валидации формата электронной почты использует регулярное выражение (или regex) для определения формата, наряду с :format аргументом для validates метода (Листинге 6.14).
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
end
Здесь регулярное выражение VALID_EMAIL_REGEX это константа, которая обозначается в Ruby именем начинающимся с большой буквы. Код
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
обеспечивает допустимость адресов электронной почты соответствующих образцу, все остальные будут считаться недопустимыми.
Так, откуда появился образец? Регулярные выражения состоят из краткого (некоторые сказали бы нечитаемого) языка для сравнения текстовых паттернов; изучение построения регулярных выражений это искусство и для начала я разбил VALID_EMAIL_REGEX на небольшие куски (рис 6.1
| /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i | полное регулярное выражение |
|---|---|
| / | начало регулярного выражения |
| \A | соответствует началу строки |
| [\w+\-.]+ | по крайней мере один символ слова, плюс, дефис или точка |
| @ | буквально "знак собаки" |
| [a-z\d\-.]+ | по крайней мере одна буква, цифра, дефис или точка |
| \. | буквальная точка |
| [a-z]+ | по крайней мере одна буква |
| \z | соответствует концу строки |
| / | конец регулярного выражения |
| i | нечувствительность к регистру |
Кстати, на самом деле существует полное регулярное выражение для сопоставления адресов электронной почты в соответствии с официальным стандартом, но волноваться не стоит. Экземпляр из Листинга 6.14 тоже хорош, возможно даже лучше чем официальный.
(рис 6.4) Удивительный редактор регулярных выражений Rubular
Теперь тесты должны пройти. (Фактически, тесты для валидных адресов электронной почты должны были проходить все время; так как регулярные выражения, как известно, подвержены ошибкам, действительные испытания электронной почты в основном заключаются в санитарной проверке на VALID_EMAIL_REGEX.) Это означает, что осталось рассмотреть только одно ограничение: обеспечение уникальности адресов электронной почты.
Для обеспечения уникальность адресов электронной почты (так, чтобы мы могли использовать их в качестве имен пользователей) мы будем использовать :unique опцию для validates метода. Но предупреждаю: есть важное предостережение, так что не просто просмотрите раздел, а прочитайте его внимательно.
Мы начнем, как обычно, с наших тестов. В наших предыдущих тестах модели мы, главным образом, использовали User.new, который только создает объект Ruby в памяти, но для тестов уникальности мы фактически должны поместить запись в базу данных.14 (Первый) тест дублирования электронной почты представлен в Листинге 6.15.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
subject { @user }
.
.
.
describe "when email address is already taken" do
before do
user_with_same_email = @user.dup
user_with_same_email.save
end
it { should_not be_valid }
end
end
Методика заключается в создании пользователя с тем же адресом электронной почты, что и у @user, чего мы достигаем с помощью @user.dup, который создает дубликат пользователя с теми же атрибутами. Поскольку мы затем сохраняем этого пользователя, оригинальный @user будет иметь адрес электронной почты который уже существует в базе данных и, следовательно, он не должен быть валидным.
Мы можем получить прохождение теста из Листинга 6.15 с кодом из Листинге 6.16.
class User < ActiveRecord::Base
.
.
.
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: true
end
И все же мы не закончили. Адреса электронной почты не чувствительны к регистру —
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
subject { @user }
.
.
.
describe "when email address is already taken" do
before do
user_with_same_email = @user.dup
user_with_same_email.email = @user.email.upcase
user_with_same_email.save
end
it { should_not be_valid }
end
end
Здесь мы используем upcase метод на строках (описан кратко в Разделе 4.3.2). Этот тест делает то же самое что и первый тест на дублирование адресов электронной почты, но с прописным адресом электронной почты. Если этот тест кажется вам немного абстрактным, запустите консоль:
$ rails console --sandbox >> user = User.create(name: "Example User", email: "user@example.com") >> user.email.upcase => "USER@EXAMPLE.COM" >> user_with_same_email = user.dup >> user_with_same_email.email = user.email.upcase >> user_with_same_email.valid? => true
Конечно, сейчас user_with_same_email.valid? является true, так как это провальный тест, но мы хотим, чтобы оно было false. К счастью, :uniqueness принимает опцию, :case_sensitive, как раз для этой цели (Листинг 6.18).
class User < ActiveRecord::Base
.
.
.
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
end
Обратите внимание: мы просто заменили true на case_sensitive: false; Rails в этом случае делает вывод, что :uniqueness должно быть true. В этой точке наше приложение обеспечивает уникальность адресов электронной почты и наш набор тестов должен пройти.
Предостережение уникальности
Есть одна небольшая проблема, предостережение, на которое я ссылался выше:
Использование validates :uniqueness не гарантирует уникальности.
D’oh! Но что может может пойти не так? А вот что:
Если вышеописанная последовательность кажется вам неправдоподобной, поверьте мне, это не так: это происходит на любом Rails сайте со значительным трафиком. К счастью, решение просто в реализации, нам просто необходимо обеспечить уникальность также на уровне базы данных. Наш метод заключается в создании в базе данных индекса столбца электронной почты и последующем требовании уникальности этого индекса.
Индекс адреса электронной почты представляет собой обновление требований к нашей модели данных что (как обсуждалось в Разделе 6.1.1) делается в Rails посредством миграций. Мы видели в Разделе 6.1.1 что генерация модели User автоматически создает новую миграцию (Листинг 6.2); в данном случае мы добавляем структуру к существующей модели, таким образом, мы должны создать миграцию непосредственно, используя migration генератор:
$ rails generate migration add_index_to_users_email
В отличие от миграции для пользователей, миграция уникальности электронной почты не предопределена, таким образом, мы должны заполнить ее содержание кодом из Листинга 6.19.
class AddIndexToUsersEmail < ActiveRecord::Migration
def change
add_index :users, :email, unique: true
end
end
Здесь используется Rails метод add_index для добавления индекса на столбце email таблицы users. Индекс сам по себе не обеспечивает уникальность, но это делает опция unique: true.
Заключительный шаг должен мигрировать базу данных:
$ bundle exec rake db:migrate
(Если это не сработало, попробуйте закрыть все консольные сессии в песочнице, которая может блокировать базу данных, тем самым препятствуя миграции.) Если вам интересно посмотреть на практический результат выполнения этой команды, посмотрите файл db/schema.rb, который теперь должен содержать строку подобную этой:
add_index "users", ["email"], name: "index_users_on_email", unique: true
К сожалению, есть еще одно изменение которое мы должны сделать для того чтобы быть уверенными в уникальности email адресов - все email адреса должны быть в нижнем регистре, прежде чем они будут сохранены в базе данных. Причина заключается в том что не все адаптеры баз данных используют регистрозависимые индексы.before_save для того чтобы принудить Rails переводить в нижний регистр email атрибут перед сохранением пользователя в базу данных, как это показано в Листинге
6.20.
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
.
.
.
end
Код в Листинге 6.20 передает блок в коллбэк before_save и назначает email адрес пользователя равным его текущему значению в нижнем регистре с помощью метода строки downcase. Этот код довольно продвинутый и в этой точке я советую вам просто поверить в то что он работает; если вы все же сомневаетесь, закомментируйте валидацию уникальности из Листинга 6.16 и попробуйте создать пользователей с идентичными email адресами для того чтобы посмотреть на результирующую ошибку. (Мы вновь увидим эту технику в Разделе 8.2.1, где мы будем использовать конвенцию method reference.) Написание теста для кода в Листинге 6.20 остается в качестве упражнения (Раздел 6.5).
Теперь вышеописанный сценарий с Алисой будет хорошо работать: база данных сохранит запись пользователя, основанную на первом запросе, и отвергнет второе сохранение за нарушение уникальности. (Ошибка появится в логе Rails, но в этом нет ничего плохого. Можно даже отловить ActiveRecord::StatementInvalid исключение, но в этом учебном руководстве мы не будем заморачиваться этим шагом.) Добавление этого индекса на атрибут адреса электронной почты преследует вторую цель, кратко рассмотренную в Разделе 6.1.4: он решает проблему эффективности поиска пользователя с помощью find_by (Блок 6.2).
_________________________________________________________
Блок 6.2.Индексы базы данных
При создании столбца в базе данных, важно учитывать, что нам нужно будет найти записи по этому столбцу. Рассмотрим, например, email атрибут созданый миграцией в Листинге 6.2. Когда мы позволим пользователям регистрироваться на сайте, начиная с Главы 7, нам нужно будет иметь возможность находить запись пользователя соответствующую предоставленному адресу электронной почты; в базе данных, основанной, к сожалению, на наивной модели данных, единственный способ найти пользователя по его адресу электронной почты, это просмотреть строку каждого пользователя в базе данных и сравнить его адрес электронной почты с атрибутом предоставленного адреса электронной почты. Это известно в бизнесе баз данных, как full-table scan (полное сканирование таблицы), а для реального сайта с тысячами пользователей это Bad Thing.
Добавление индекса к столбцу электронной почты решает проблему. Чтобы понять индекс базы данных, полезно рассмотреть аналогию с индексом книги. В книге, чтобы найти все вхождения заданной строки, например "foobar", вам придется сканировать каждую страницу в поиске "foobar". С индексом книги, с другой стороны, вы можете просто посмотреть "foobar" в индексе, чтобы увидеть все страницы, содержащие "foobar". Индекс базы данных работает, по сути, аналогичным образом.
_________________________________________________________
В этом разделе мы добавим последний из базовых атрибутов модели User: безопасный пароль используемый для аутентификации пользователей примера приложения. Методика заключается в запросе у каждого пользователя пароля (с подтверждением), а затем сохранении зашифрованной версии пароля в базе данных. Мы также добавим способ аутентификации пользователя опирающийся на данный пароль, метод, который мы будем использовать в Главе 8 для того чтобы дать пользователям возможность входить на сайт.
Метод для аутентификации будет принимать введеный пароль, шифровать его и сравнивать зашифрованное значение с хранящимся в базе данных. Если они совпадают, то введеный пароль корректен и пользователь аутентифицирован. Сравнивая зашифрованные пароли вместо того чтобы сравнивать пароли непосредственно, мы получаем возможность аутентифицировать пользователей не храня в базе данных сами пароли, тем самым избежав серьезной дыры в системе безопасности нашего приложения.
БОльшая часть механики безопасного пароля будет реализована с помощьо одного Rails-метода называемого has_secure_password (впервые был представлен в Rails 3.1). Поскольку очень многое в дальнейшем зависит от этого единственного метода, трудно разрабатывать безопасные пароли постепенно. Начиная с Раздела 6.3.2 я рекомендую добавить метод has_secure_password заранее, а затем комментировать его перед добавлением каждого нового теста для того чтобы обеспечить правильный цикл TDD. (Поскольку скринкасты позволяют демонстрировать более постепенный подход к разработке, заинтересованным читателям следует посмотреть Ruby on Rails Tutorial screencasts для более полного понимания этого материала.)
Мы начнем с необходимого изменения модели данных для пользователей, что подразумевает добавление ). Название digest пришло из терминологии криптографических хэш функций, а само имя password_digest необходимо для работы реализации в Разделе 6.3.4. Как следует зашифровав пароль, мы обеспечим невозможность получения доступа к сайту атакером, даже если он умудрится получить копию базы данных.
(рис 6.5) Модель User с добавленным атрибутом password_digest.
Мы будем использовать самую новомодную хэш функцию, называемую bcrypt для необратимого шифрования пароля в виде хэша пароля. Для того чтобы использовать bcrypt в примере приложения нам необходимо добавить гем bcrypt-ruby в наш Gemfile (Листинг 6.21).
source 'https://rubygems.org' ruby '2.0.0' #ruby-gemset=railstutorial_rails_4_0 gem 'rails', '4.0.2' gem 'bootstrap-sass', '2.3.2.0' gem 'bcrypt-ruby', '3.1.2' . . .
Затем запускаем bundle install:
$ bundle install
Поскольку мы хотим чтобы пользователи имели столбец password digest, объект user должен отвечать на password_digest, что приводит нас к тесту показанному в Листинге 6.22.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
.
.
.
end
Для того чтобы получить прохождение этого теста мы вначале генерируем соответствующую миграцию для столбца password_digest:
$ rails generate migration add_password_digest_to_users password_digest:string
Здесь первый аргумент это название миграции и мы также предоставили второй аргумент с названием и типом атрибута который мы хотим создать. (Сравните это с начальной генерацией таблицы users в Листинге 6.1.) Мы можем выбрать любое название для миграции, но было бы удобно, если бы ее название заканчивалось на _to_users, поскольку в этом случае Rails автоматически построит миграцию для добавления столбцов к таблице users. Кроме того, включив второй аргумент, мы дали Rails достаточно информации для построения для нас всей миграции, как это видно в Листинге 6.23.
class AddPasswordDigestToUsers < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
end
end
Этот код использует метод add_column для добавления столбца password_digest к таблице users.
Мы можем получить прохождение провального теста из Листинга 6.22 запустив миграцию базы данных разработки и подготовив тестовую базу данных:
$ bundle exec rake db:migrate $ bundle exec rake test:prepare $ bundle exec rspec spec/
Как видно на наброске рис 6.1, мы ожидаем что пользователи должны будут подтверждать свои пароли, что является общепринятой в сети практикой минимизирующей риск опечаток при введении пароля. Мы можем реализовать это на уровне контроллера, но принято делать это в модели и использовать Active Record для наложения этого ограничения. Метод заключается в добавлении password и password_confirmation атрибутов к модели User и последующем требовании совпадения этих двух атрибутов перед сохранением записи в базе данных. В отличие от всех остальных атрибутов, что мы видели до этого, атрибуты пароля будут виртуальными — они будут лишь временно существовать в памяти и не будут постоянно храниться в базе данных. Как мы увидим в Разделе 6.3.4, has_secure_password реализует эти виртуальные атрибуты автоматически .
Мы начнем с respond_to тестов для пароля и его подтверждения, как это показано в Листинге 6.24.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should be_valid }
.
.
.
end
обратите внимание - мы добавили :password и :password_confirmation в хэш инициализации для User.new:
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
Мы определенно не хотим чтобы пользователи могли вводить пустые пароли, так что мы добавим еще один тест для валидации наличия пароля:
describe "when password is not present" do
before { @user.password = @user.password_confirmation = " " }
it { should_not be_valid }
end
Поскольку мы будем вскоре тестировать несовпадение пароля, здесь мы будем тестировать существование валидации наличия установив пароль и его подтверждение равными чистой строке.
Мы также хотим убедиться что пароль и его подтверждение совпадают. Случай, когда они совпадают покрыт с помощью it { should be_valid }, так что нам осталось протестировать только случай несовпадения:
describe "when password doesn't match confirmation" do
before { @user.password_confirmation = "mismatch" }
it { should_not be_valid }
end
Собрав все вместе, мы получаем (провальные) тесты в Листинге 6.25.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should be_valid }
.
.
.
describe "when password is not present" do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: " ", password_confirmation: " ")
end
it { should_not be_valid }
end
describe "when password doesn't match confirmation" do
before { @user.password_confirmation = "mismatch" }
it { should_not be_valid }
end
end
Мы можем получить прохождение тестов из Листинга 6.25 используя лишь одну строку кода - как это показано в Листинге 6.26.
class User < ActiveRecord::Base . . . has_secure_password end
Примечательно что одна линия
has_secure_password
приводит к прохождению всех текущих тестов пароля. На самом деле она делает гораздо больше, даже слишком много, мешая будущим тестам краснеть перед позеленением, так что прежде чем двигаться дальше я рекомендую закоментировать эту строку (Листинге 6.27).
class User < ActiveRecord::Base . . . # has_secure_password end
Последняя часть механики наших паролей это метод для получения пользователей по их email и паролям. Эта задача естественным образом разбивается на две части: первая из них это поиск пользователя по адресу электронной почты; вторая это аутентификация пользователя с данным паролем. Все тесты (кроме последнего) в этом разделе связаны с has_secure_password, так что в процессе реализации вы должны иметь возможность раскомментировать закомментированную строку из Листинге 6.27 для того чтобы дать тестам возможность пройти.
Первый шаг прост; как мы видели в Разделе 6.1.4, мы можем найти пользователя с данным адресом электронной почты с помощью метода find_by:
user = User.find_by(email: email)
Второй шаг заключается в применении метода authenticate для проверки того что у пользователя есть данный пароль. В Главе 8, мы будем получать текущего (вошедшего) пользователя используя код вроде этого:
current_user = user.authenticate(password)
Если данный пароль совпадает с паролем пользователя, он должен вернуть пользователя; в противном случае он должен вернуть false.
Как обычно, мы можем выразить требования для authenticate используя RSpec. Получившиеся в результате тесты являются немного более продвинутыми чем те что мы видели до этого, так что давайте разобьем их на части; если вы новичок в RSpec, вам возможно понадобится прочитать этот раздел несколько раз. Мы начнем с того, что объект User должен отвечать на authenticate:
it { should respond_to(:authenticate) }
Затем мы покрываем два случая - совпадение и несовпадения пароля:
describe "return value of authenticate method" do
before { @user.save }
let(:found_user) { User.find_by(email: @user.email) }
describe "with valid password" do
it { should eq found_user.authenticate(@user.password) }
end
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not eq user_for_invalid_password }
specify { expect(user_for_invalid_password).to be_false }
end
end
Блок before сохраняет пользователя в базе данных, так что он может быть получен с помощью find_by, чего мы достигаем используя let method:
let(:found_user) { User.find_by(email: @user.email) }
Мы уже использовали let в нескольких упражнениях, это первый случай когда мы его видим в основном тексте учебника. Блок 6.3 рассказывает о let более подробно.
Два блока describe покрывают случаи когда @user и found_user должны быть одинаковыми (совпадение пароля) и разными (несовпадение пароля); они используют "равенство" eq для проверки эквивалентности объектов (который в свою очередь использует ==, как мы видели в Разделе 4.3.1). Обратите внимание что тесты в
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not eq user_for_invalid_password }
specify { expect(user_for_invalid_password).to be_false }
end
используют let второй раз, а также используют метод specify. Это просто синоним для it, который может быть использован когда it звучит ненатурально
___________________________________________
Блок 6.3.Применение let
RSpec метод let обеспечивает удобный способ для создания локальных переменных внутри тестов. Синтаксис может выглядеть немного странно, но его эффект аналогичен назначению переменной. Аргументом let является символ и он принимает блок, который возвращает значение назначаемое локальной переменной с именем символа. Другими словами,
let(:found_user) { User.find_by(email: @user.email) }
создает переменную found_user чье значение эквивалентно результату find_by. Затем мы можем использовать эту переменную в любом before или it блоке на протяжении всего оставшегося теста. Одно из достоинств let заключается в том, что он мемоизирует свое значение, это означает что он запоминает значение от одного вызова до другого. (Обратите внимание, что memoize это технический термин; в частности, его не надо путать с "memorize".) В данном случае, поскольку let мемоизирует переменную found_user, метод find_by будет вызван лишь единожды при запуске спеков модели User.
___________________________________________
Наконец, в качестве дополнительной меры предосторожности, мы протестируем на наличие валидации длины паролей, установив длину паролей не меньшей чем шесть знаков:
describe "with a password that's too short" do
before { @user.password = @user.password_confirmation = "a" * 5 }
it { should be_invalid }
end
Собрав вместе все тесты мы получаем Листинге 6.28.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
.
.
.
it { should respond_to(:authenticate) }
.
.
.
describe "with a password that's too short" do
before { @user.password = @user.password_confirmation = "a" * 5 }
it { should be_invalid }
end
describe "return value of authenticate method" do
before { @user.save }
let(:found_user) { User.find_by(email: @user.email) }
describe "with valid password" do
it { should eq found_user.authenticate(@user.password) }
end
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not eq user_for_invalid_password }
specify { expect(user_for_invalid_password).to be_false }
end
end
end
Как было отмечено в Блоке 6.3, let мемоизирует свое значение, так что первый вложенный describe блок в Листинге 6.28 вызывает let для получения пользователя из базы данных с помощью find_by, но второй describe блок уже не обращается к базе данных.
В предыдущей версии Rails, добавление безопасного пароля было сложным и долгим, как это можно увидеть в http://railstutorial.ru/chapters/3_0/beginning
Во-первых, нам нужна валидация длины для пароля которая использует ключ :minimum по аналогии с ключом :maximum из Листинга 6.12:
validates :password, length: { minimum: 6 }
(Валидации наличия и подтверждения автоматически добавляются has_secure_password.)
Во-вторых, нам нужно добавить к атрибутам password и password_confirmation требование наличия пароля, требование их совпадения и добавить authenticate метод для сравнения зашифрованного пароля с password_digest для аутентификации пользователей. Это единственный непростой шаг и в последней версии Rails все эти фичи бесплатно поставляются в одном методе - has_secure_password:
has_secure_password
Пока столбец password_digest присутствует в базе данных, добавление одного лишь этого метода к нашей модели дает нам безопасный способ для создания и аутентификации новых пользователей.
(Если вы хотите увидеть как реализован has_secure_password, я советую взглянуть на хорошо документированый и вполне читабельный исходный код secure_password.rb. Этот код включает строки
validates_confirmation_of :password,
if: lambda { |m| m.password.present? }
которые (как описано в Rails API) автомагически создают атрибут password_confirmation. Он также включает валидацию для password_digest атрибута.
Совместно с валидацией наличия из Листинга 6.26, вышеприведенные элементы приводят к модели User показаной в Листинге 6.29, который завершает реализацию безопасных паролей.
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true,
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
end
Теперь необходимо убедиться что набор тестов проходит:
$ bundle exec rspec spec/
Примечание: Если вы получили deprecation warning вроде
[deprecated] I18n.enforce_available_locales will default to true in the future
вы можете попробовать избавиться от него отредактировав config/application.rb следующим образом:
require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
class Application < Rails::Application
.
.
.
I18n.enforce_available_locales = true
.
.
.
end
end
Теперь, когда базовая модель User завершена, мы создадим пользователя в базе данных в качестве подготовки к созданию страницы показывающей информацию о пользователе в Разделе 7.1. Это также даст нам шанс сделать работу, проделанную в предыдущих разделах, более ощутимой; одни лишь проходяшие тесты могут показаться разочаровывающим результатом - гораздо приятнее увидеть настоящую запись пользователя в базе данных.
Поскольку мы пока не можем зарегистрироваться через веб-интерфейс — это является целью Главы 7 — мы будем использовать Rails консоль для создания нового пользователя вручную. В отличие от Раздела 6.1.3, в этом разделе мы не должны работать в песочнице, поскольку в этот раз нашей целью является сохранение записи в базе данных:
$ rails console >> User.create(name: "Michael Hartl", email: "mhartl@example.com", ?> password: "foobar", password_confirmation: "foobar") => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 20:45:19", updated_at: "2013-03-11 20:45:19", password_digest: "$2a$10$kn4cQDJTzV76ZgDxOWk6Je9A0Ttn5sKNaGTEmT0jU7.n...">
Для того чтобы проверить что это работает, давайте взглянем на строку в девелопмент базе данных (). Обратите внимание, что столбцы соответствуют атрибутам модели данных, определенной на рис 6.5.
(рис 6.6) Строка пользователя в базе данных SQLite db/development.sqlite3.
Вернувшись в консоль, мы можем увидеть эффект has_secure_password из Листинга 6.29 посмотрев на атрибут password_digest:
>> user = User.find_by(email: "mhartl@example.com") >> user.password_digest => "$2a$10$kn4cQDJTzV76ZgDxOWk6Je9A0Ttn5sKNaGTEmT0jU7.ncBJ/60gHq"
Это зашифрованная версия пароля ("foobar") которая была использована для инициализации объекта user. Мы также можем проверить что команда authenticate работает, использовав вначале невалидный, а затем валидный пароли:
>> user.authenticate("invalid")
=> false
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2013-03-11 20:45:19", updated_at: "2013-03-11 20:45:19",
password_digest: "$2a$10$kn4cQDJTzV76ZgDxOWk6Je9A0Ttn5sKNaGTEmT0jU7.n...">
Как и требовалось тестами в Листинге 6.28, authenticate возвращает false если пароль невалиден и самого пользователя в противном случае.
Начав с нуля, в этой главе мы создали рабочую модель User с name, email и различными "парольными" атрибутами, вместе с валидациями обеспечивающими несколько важных ограничений на их значения. Кроме того, мы можем безопасно аутентифицировать пользователей с помощью пароля. В предыдущих версиях Rails такой подвиг потребовал бы в два раза большего количества кода, но благодаря компактному методу validates и has_secure_password, мы смогли построить рабочую модель User используя меньше дюжины строк исходного кода.
В следующей (седьмой) главе мы сделаем рабочую форму регистрации для создания новых пользователей, вместе со страницей для отображения информации о каждом пользователе. В Главе 8 мы будем использовать механизм аутентификации из Раздела 6.3 для того чтобы дать пользователям возможность входить на сайт.
Если вы используете Git, было бы неплохо закоммитить внесенные изменения, если вы этого еще не сделали:
$ git add . $ git commit -m "Make a basic User model (including secure passwords)"
Затем опять объединить их с мастер веткой:
$ git checkout master $ git merge modeling-users
reload для перезагрузки значения из базы данных и метод eq для тестирования на равенство. Закомменировав строку before_save убедитесь что тесты из Листинга 6.30 тестируют правильные вещи.before_save может быть написан способом показанным в Листинге 6.31.ActiveRecord::Base для того чтобы получить представление о ее возможностях.validates для того чтобы больше узнать о его возможностях и опциях.require 'spec_helper'
describe User do
.
.
.
describe "email address with mixed case" do
let(:mixed_case_email) { "Foo@ExAMPle.CoM" }
it "should be saved as all lower-case" do
@user.email = mixed_case_email
@user.save
expect(@user.reload.email).to eq mixed_case_email.downcase
end
end
.
.
.
end
class User < ActiveRecord::Base
has_secure_password
before_save { email.downcase! }
.
.
.
end
class User < ActiveRecord::Base
.
.
.
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
.
.
.
end
Главу 5 мы закончили созданием страницы-заглушки для регистрации пользователей (Раздел 5.4); в течение следующих четырех глав мы выполним обещание, неявное в этой начинающейся странице регистрации. Первый важный шаг это создание модели данных для пользователей нашего сайта, вместе со способом хранить эти данные. В Главе 7 мы дадим пользователям возможность регистрироваться на нашем сайте и создадим страницу профиля пользователя. Как только пример приложения сможет создавать новых пользователей, мы также позволим им входить и выходить (Глава 8) и в Главе 9 (Раздел 9.2.1) мы узнаем как защитить страницы от несанкцонированного доступа. Взятые вместе, материалы с Главы 6 по Главу 9 разрабатывают полную Rails систему входа и аутентификации. Как вы, возможно, знаете, для Rails существует множество готовых решений для аутентификации; Блок 6.1 поясняет почему разворачивание собственной системы является лучшей идеей.
Это длинная и насыщенная действиями глава и вам она может показаться необычайно сложной, особенно если вы новичок в моделировании данных. Тем не менее, по ее окончании мы создадим весьма качественную систему для валидации, хранения и извлечения информации о пользователе.
___________________________________________
Блок 6.1.Прикручивание собственной системы аутентификации
Фактически всем веб-приложениям в настоящее время требуется какая либо система входа и аутентификации. Неудивительно, что у большинства веб-фреймворков есть множество вариантов реализации подобных систем, и Rails не исключение. Примеры систем аутентификации и авторизации включают в себя Clearance, Authlogic, Devise и CanCan (так же как не-Rails-специфичные решения, построенные на основе OpenID или OAuth). Резонный вопрос - почему мы должны изобретать велосипед. Почему бы просто не использовать готовое решение вместо того чтобы прикручивать свое?
С одной стороны, практика показывает, что аутентификация на множестве сайтов требует серьезной кастомизации и модификация стороннего продукта, это, обычно, даже большая работа чем написание собственной системы с нуля. К тому же, готовые решения это "черные ящики", с весьма загадочными внутренностями; а когда вы пишете свою собственную систему у вас гораздо больше шансов разобраться в ней. Кроме того, последние дополнения к Rails (Раздел 6.3) очень облегчили написание собственной системы аутентификации. Наконец, если вы все же когда-либо решите использовать стороннюю систему, вам будет гораздо проще в ней разобраться если вы прежде имели опыт написания собственной .
___________________________________________
(Первая строка здесь только для того чтобы удостовериться, что вы находитесь на master ветке, чтобы тема ветки modeling-users была основана на master ветке. Можно пропустить эту команду, если вы уже находитесь в master ветке.)
Хотя конечная цель следующих трех глав это создание страницы регистрации для нашего сайта (ее набросок показан на рис 6.1), в принятии регистрационной информации сейчас хорошего мало, так как нам в настоящий момент попросту негде ее хранить. Таким образом, первый шаг в регистрации пользователей должен создать структуру данных для получения и хранения их информации.
(рис 6.1) Набросок страницы регистрации пользователей.
В Rails дефолтную структуру данных для модели данных называют, что достаточно естественно, модель (М. в MVC из Раздела 1.2.6). Дефолтное решение Rails для проблемы персистентности состоит в том, чтобы использовать базу данных для долгосрочного хранения данных и дефолтную библиотеку Active Record для взаимодействия с базой данных.
Можно вспомнить из Раздела 4.4.5 , что мы уже встречали, в созданном нами классе User объекты user с атрибутами name и email. Тот класс служил полезным примером, но он испытывал недостаток в критическом свойстве персистентности: когда мы создали объект User в консоли Rails, он исчез, как только мы вышли. Наша цель в этом Разделе состоит в том, чтобы создать модель для пользователей, которые не будут исчезать так легко.
Как и с классом User в Разделе 4.4.5, мы начнем с моделирования пользователя с двумя атрибутами:
class User attr_accessor :name, :email . . . end
Напротив, при использовании Rails, для моделирования пользователей мы не должны идентифицировать атрибуты явно. Как было кратко отмечено выше, для хранения данных Rails по умолчанию использует реляционные базы данных, которые состоят из таблиц составленных из строк, данных, где у каждой строки есть столбцы атрибутов данных. Например, для того, чтобы сохранить пользователей с именами и адресами электронной почты, мы составим таблицу users со столбцами name и email (с каждой строкой, соответствующей одному пользователю). Называя столбцы таким образом, мы позволяем Active Record выводить атрибуты объектов User для нас.
Давайте посмотрим как это работает. (Если это обсуждение становится слишком абстрактным на ваш взгляд, будьте терпеливы; консольные примеры, начинающиеся в Разделе 6.1.3 и скриншоты браузера базы данных на рис 6.3 и рис 6.6 должны многое прояснить.) Вспомните из Листинге 5.31 что мы создавали контроллер Users (наряду с new действием) используя команду
$ rails generate controller Users new --no-test-framework
Есть аналогичная команда для создания модели: generate model. Листинге 6.1 показывает команду для генерации модели User с двумя атрибутами, name и email.
$ rails generate model User name:string email:string
invoke active_record
create db/migrate/[timestamp]_create_users.rb
create app/models/user.rb
invoke rspec
create spec/models/user_spec.rb
(Обратите внимание, что, в отличие от множественного соглашения для имен контроллеров, названия моделей - в ед. числе: контроллер Users, но модель User.) Передавая дополнительные параметры name:string и email:string, мы говорим Rails о двух желаемых атрибутах, наряду с тем, какого типа эти атрибуты должны быть (в данном случае, string). Сравните это с включением имен действий в Листинге 3.4 и Листинге 5.31.
Одним из результатов generate команды в Листинге 6.1 является новый файл, названный migration. Миграции обеспечивают возможность постепенного изменения структуры базы данных, так, чтобы наша модель данных могла адаптироваться к изменяющимся требованиям. В случае модели User, миграция создается автоматически сценарием генерации модели; что создает таблицу users с двумя столбцами, name и email, как это показано в Листинге 6.2. (Мы увидим в Разделе 6.2.5 и еще раз в Разделе 6.3 как создавать миграцию с нуля.)
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
Обратите внимание: у названия файла миграции есть префикс в виде временнОй отметки основанной на времени генерации миграции. В первые дни миграций, названия файлов имели префиксы в виде увеличивающихся целых чисел, что приводило к конфликтам в командах разработчиков в случаях когда несколько программистов создавали миграции с совпадающими номерами. Использование временнЫх меток позволило комфортно избегать подобных коллизий.
Сама миграция представляет собой метод
(рис 6.2) Модель данных 'пользователи', произведенная Листингом 6.2.
Мы можем запустить миграцию, известную как "migrating up", используя rake команду (Блок 2.1) следующим образом:
$ bundle exec rake db:migrate
(Можно вспомнить, что мы запускали эту команду прежде, в Разделе 2.2.) При первом запуске ); сравните со схемой на рис 6.2. Вы могли отметить, что есть один столбец в рис 6.3 неучтенный в миграции: столбец id. Как было вкратце отмечено в Разделе 2.2, этот столбец создается автоматически, и используется Rails в качестве уникального идентификатора каждой строки.
(рис 6.3) SQLite Database Browser с нашей новой users таблицей.
Большинство миграций являются обратимыми, а это означает что мы можем "migrate down" и переделать ее с помощью единственной Rake задачи, называемой db:rollback:
$ bundle exec rake db:rollback
(См. в Блоке 3.2 еще одну полезную для обращения миграций технику.) Под капотом этой Rake задачи происходит выполнение команды drop_table для удаления таблицы users из базы данных. Причина по которой это работает кроется в том, что метод change знает что drop_table это команда обратная create_table и это означает что способ отката миграции легко определим. В случае необратимых миграций, таких как удаление столбца из базы данных, необходимо определять отдельные up и down методы вместо единственного метода change. Почитайте о миграциях в (rus)Rails Guides дабы составить о них более полное представление.
Если вы откатывали базу данных, migrate up снова перед продолжением:
$ bundle exec rake db:migrate
Мы видели, как генерация модели User в Листинге 6.1 сгенерировала файл миграции (Листинг 6.2) и мы видели на рис 6.1" type="example">Листинге 6.1 сгенерировала файл миграции (Листинг 6.2) и мы видели на users со столбцами id, name, email, created_at и updated_at. Листинг 6.1 также создал саму модель; остальная часть этого раздела посвящена ее изучению.
Мы начнем с рассмотрения кода для модели User, которая живет в файле user.rb в каталоге app/models/ это, мягко выражаясь, очень компактно (Листинг 6.3).
class User < ActiveRecord::Base end
Вспомните из Раздела 4.4.2 что синтаксис class User < ActiveRecord::Base означает что класс User наследует от ActiveRecord::Base, таким образом, у модели User автоматически есть вся функциональность ActiveRecord::Base класса. Конечно, знание этого наследования не приносит пользы, если мы не знаем что содержит ActiveRecord::Base так что давайте начнем с конкретных примеров.
Как и в Главе 4, наш инструмент - консоль Rails. Так как мы (пока) не хотим производить какие либо изменения в нашей базе данных, мы запустим консоль в
$ rails console --sandbox Loading development environment in sandbox Any modifications you make will be rolled back on exit >>
Как обозначено полезным сообщением "Любые модификации которые вы сделаете откатятся при выходе", при работе в песочнице, консоль будет "откатывать" (то есть, отменять) любые изменения базы данных, созданные во время сеанса.
В консольной сессии в Разделе 4.4.5 мы создавали нового пользователя с User.new, к которому мы имели доступ только после подгрузки файла example user из Листинга 4.9. С моделями ситуация иная; как вы можете вспомнить из Раздела 4.4.4, Rails консоль автоматически загружает окружение Rails, которое включает модели. Это означает что мы можем создавать новые объекты user без необходимости подгружать что либо:
>> User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
Мы видим здесь дефолтное представление объекта user.
Вызванный без параметров, User.new возвращает объект с nil атрибутами. В Разделе 4.4.5 мы спроектировали пример класса User таким образом, чтобы он принимал инициализационный хэш для установки атрибутов объекта; такое решение было обусловлено библиотекой Active Record, которая позволяет инициализировать объекты тем же способом:
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com") => #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil>
Здесь мы видим, что, как и ожидалось, атрибуты имени и адреса электронной почты были установлены.
Если вы следили за development log, вы, возможно, заметили, что новые строки еще не обнаружились. Это связано с тем, что вызов User.new не касается базы данных; он просто создает новый Ruby объект в памяти. Чтобы сохранить объект user в базе данных, мы вызовем метод save на переменной user:
>> user.save => true
Метод save возвращает true если сохранение успешно выполнилось и false если сохранение не выполнено. (Сейчас все сохранения должны успешно выполняться; но в Разделе 6.2 мы увидим случаи, когда некоторые из них не сработают.) После сохранения в логе консоли должна появиться строка с командой SQL INSERT INTO "users". Из-за множества методов, предоставляемых Active Record, в этой книге нам не потребуется необработанный SQL и я буду опускать обсуждение команд SQL с этого момента. Но вы можете многому научиться, читая SQL соответствующий командам Active Record.
Вы, возможно, заметили что у нового объекта user были nil значения для атрибутов id и волшебных столбцов created_at и updated_at. Давайте посмотрим, изменило ли наше save что-нибудь:
>> user => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">
Мы видим что 2013-03-11 00:57:46" вызвало ваше любопытство - я не пишу это после полуночи; временнЫе метки записаны во Всемирном координированном времени (UTC), которое для многих практических целей является аналогом Среднего времени по Гринвичу. Из NIST Time and Frequency FAQ: Q: Почему UTC используется в качестве акронима к Coordinated Universal Time вместо CUT? A: В 1970 система Coordinated Universal Time была разработана международной консультативной группой технических экспертов в рамках International Telecommunication Union (ITU). ITU чувствовал, что было лучше определить единственное сокращение для использования на всех языках, чтобы минимизировать беспорядок. Так как единогласное соглашение не могло быть достигнуто при использовании английского порядка слов, CUT, или французского порядка слов, TUC, акроним, UTC был
выбран в качестве компромисса.
Как и с классом User в Разделе 4.4.5, экземпляры модели User предоставляют доступ к своим атрибутам, используя точку:user.updated_at говорит вам о том что временнАя метка была в UTC
>> user.name => "Michael Hartl" >> user.email => "mhartl@example.com" >> user.updated_at => Mon, 11 Mar 2013 00:57:46 UTC +00:00
Как мы увидим в Главе 7, часто бывает удобно создать и сохранить модель в два приема, как мы это сделали выше, но Active Record также позволяет вам объединить эти действия в один шаг с User.create:
>> User.create(name: "A Nother", email: "another@example.org") #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2013-03-11 01:05:24", updated_at: "2013-03-11 01:05:24"> >> foo = User.create(name: "Foo", email: "foo@bar.com") #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2013-03-11 01:05:42", updated_at: "2013-03-11 01:05:42">
Обратите внимание: User.create, вместо того чтобы возвратить true или false, возвращает сам объект User который мы можем дополнительно присвоить переменной (такой как foo во второй команде выше).
Команда, обратная create это destroy:
>> foo.destroy => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2013-03-11 01:05:42", updated_at: "2013-03-11 01:05:42">
Странно, destroy, как и create, возвращает рассматриваемый объект, хотя я не могу вспомнить что когда-либо использовал значение, возвращаемое destroy. Еще более странно то, что destroyенный объект все еще существует в памяти:
>> foo => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2013-03-11 01:05:42", updated_at: "2013-03-11 01:05:42">
Как мы узнаем, уничтожили ли мы в действительности объект? И как мы можем получить сохраненные и неуничтоженные объекты user из базы данных? Пора узнать, как использовать Active Record, для поиска объектов user.
Active Record предоставляет несколько способов поиска объектов. Давайте используем их, для того, чтобы найти первого пользователя, которого мы создали, и чтобы проверить, что третий пользователь (foo) был уничтожен. Мы начнем с существующего пользователя:
>> User.find(1) => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">
Здесь мы передали id пользователя в User.find; Active Record вернула пользователя с этим атрибутом id.
Давайте посмотрим, существует ли пользователь с id 3 в базе данных:
>> User.find(3) ActiveRecord::RecordNotFound: Couldn't find User with ID=3
Так как мы уничтожили нашего третьего пользователя в Разделе 6.1.3, Active Record не может найти его в базе данных. Вместо этого
В дополнение к универсальному find, Active Record также позволяет нам искать пользователей по определенным атрибутами:
>> User.find_by_email("mhartl@example.com")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">
Метод find_by_email автоматически создается библиотекой Active Record на основе email атрибута в таблице users. (Как вы догадываетесь, Active Record также создает метод find_by_name.) Начиная с Rails 4.0, более предпочтительным способом для поиска атрибута является использование метода find_by с атрибутом передаваемым в виде хэша:
>> User.find_by(email: "mhartl@example.com") => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">
Поскольку мы будем использовать адреса электронной почты в качестве имен пользователей, этот вид find будет полезен когда мы узнаем как позволить пользователям регистрироваться на нашем сайте (Глава 7). Если вы беспокоитесь об эффективности find_by при большом количестве пользователей - ваше беспокойство вполне обоснованно, но вы немного забегаете вперед; мы обсудим эту проблему и ее решение в Разделе 6.2.5.
Мы закончим несколькими более общими способами поиска пользователей. Во-первых, first:
>> User.first => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">
стественно, first просто возвращает первого пользователя в базе данных. Есть также all:
>> User.all => [#<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">, #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2013-03-11 01:05:24", updated_at: "2013-03-11 01:05:24">]
Не сюрприз, что all возвращает массив (Раздел 4.3.1) всех пользователей в базе данных.
После создания объектов мы зачастую хотим их обновить. Есть два основных способа сделать это. Во-первых, мы можем присвоить атрибуты индивидуально, как мы это делали в Разделе 4.4.5:
>> user # Просто напоминание о наших атрибутах пользователя => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46"> >> user.email = "mhartl@example.net" => "mhartl@example.net" >> user.save => true
Обратите внимание: заключительный шаг необходим, для того чтобы записать изменения в базу данных. Мы можем увидеть, что произойдет без сохранения, используя reload, которая перезагружает объекты, опираясь на информацию в базе данных:
>> user.email => "mhartl@example.net" >> user.email = "foo@bar.com" => "foo@bar.com" >> user.reload.email => "mhartl@example.net"
Теперь, когда мы обновили пользователя, выполнив user.save, волшебные столбцы отличаются, как и обещалось в Разделе 6.1.3:
>> user.created_at => "2013-03-11 00:57:46" >> user.updated_at => "2013-03-11 01:37:32"
Второй основной способ обновления атрибутов заключается в использовании update_attributes:
>> user.update_attributes(name: "The Dude", email: "dude@abides.org") => true >> user.name => "The Dude" >> user.email => "dude@abides.org"
Update_attributes метод принимает хеш атрибутов и в случае успеха выполняет и обновление, и сохранение за один шаг (возвращающая true чтобы указать что сохранение произошло). Обратите внимание - если какая-нибудь из валидаций выдаст ошибку, что может случиться если, например, пароль был указан как обязательный для сохранения записи (как это реализовано в Разделе 6.3), вызов update_attributes провалится и обновления записи не произойдет. Если мы хотим обновить только один атрибут, то использование сингулярного update_attribute позволит обойти это ограничение:
>> user.update_attribute(:name, "The Dude") => true >> user.name => "The Dude"
У модели User, которую мы создали в Разделе 6.1 теперь есть рабочие атрибуты name и email, но они абсолютно универсальны: любая строка (включая пустую) в настоящий момент допустима. И все же, имена и адреса электронной почты это нечто более определенное. Например, name не должно быть пробелом, email должен соответствовать определенному формату, характерному для адресов электронной почты. Кроме того, так как мы будем использовать адреса электронной почты в качестве уникальных имен пользователей при регистрации, мы не должны позволять дублироваться адресам электронной почты в базе данных.
Короче говоря, мы не должны позволить name и email быть просто любыми строками; мы должны реализовать определенные ограничения для их значений. Active Record позволяет нам налагать такие ограничения, с помощью validations. В этом разделе мы рассмотрим несколько из наиболее распространенных случаев, применив валидации для наличия, длины, формата и уникальности. В Разделе 6.3.4 мы добавим заключительную общепринятую валидацию - подтверждение. И мы увидим в Разделе 7.3 как валидации дают нам удобные сообщения об ошибках когда пользователи предоставляют данные которые нарушают их.
Как и все прочие фичи этого примера приложения, мы добавим валидации модели User с помощью разработки через тестирование. Поскольку мы не передали флаг
--no-test-framework
при генерации модели User (в отличие, например, от Листинга 5.31), команда в Листинге 6.1 создала начальные спеки для тестирования пользователей, но в данном случае они практически пусты (Листинг 6.4).
require 'spec_helper'
describe User do
pending "add some examples to (or delete) #{__FILE__}"
end
Здесь просто используется метод pending для указания на то, что мы должны заполнить спек чем-нибудь полезным. Мы cможем увидеть результат его применения подготовив (пустую) тестовую базу данных и запустив спек модели User:
$ bundle exec rake db:migrate $ bundle exec rake test:prepare $ bundle exec rspec spec/models/user_spec.rb * Finished in 0.01999 seconds 1 example, 0 failures, 1 pending Pending: User add some examples to (or delete) /Users/mhartl/rails_projects/sample_app/spec/models/user_spec.rb (Not Yet Implemented)
На большинстве систем, ожидающие спеки отображаются желтыми, для указания на то что они находятся как бы между проходящими (зеленый) и провальными (красный) тестами.
Это наша первая встреча с командой которая создает тестовую базу данных с корректной структурой:
$ bundle exec rake test:prepare
Это просто обеспечивает соответствие между моделью данных базы данных для разработки в rake test:prepare - возможно это решит проблему.)
Мы последуем совету дефолтного спека и заполним его небольшим количеством RSpec примеров, как это показано в Листинге 6.5.
require 'spec_helper'
describe User do
before { @user = User.new(name: "Example User", email: "user@example.com") }
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
end
Блок before, который мы видели в Листинге 5.30), запускает код внутри блока перед каждым тестом, в данном случае, создавая новую переменную экземпляра @user с помощью User.new и валидного инициализационного хэша. Затем
subject { @user }
делает @user дефолтным cубъектом тестирования, как мы это видели прежде в контексте переменной page в Разделе 5.3.4.
Два теста в Листинге 6.5 тестируют на наличие name и email атрибутов:
it { should respond_to(:name) }
it { should respond_to(:email) }
Сами по себе эти тесты не особенно полезны, так как объект User у которого нет (например) атрибута name бросит исключение в блоке before. Но эти тесты проверяют что конструкции user.name и user.email являются валидными, в то время как блок before лишь тестирует атрибуты когда они передаются в виде хэша методу new. К тому же, тестирование атрибутов модели это полезная конвенция, так как она позволяет нам с первого взгляда увидеть методы на которые должна отвечать модель.
Методы respond_to неявно используют Ruby метод respond_to? который принимает символ и возвращает true в случае если объект отвечает на данный метод или атрибут и возвращает false в противном случае:
$ rails console --sandbox >> user = User.new >> user.respond_to?(:name) => true >> user.respond_to?(:foobar) => false
(Вспомните из Раздела 4.2.3 что Ruby использует знак вопроса для обозначения таких true/false булевых методов.) Сами тесты опираются на булевую конвенцию используемую RSpec: код
@user.respond_to?(:name)
может быть протестирован с помощью такого RSpec кода
it "should respond to 'name'" do expect(@user).to respond_to(:name) end
Благодаря subject { @user }, мы можем написать это в альтернативном однострочном стиле, впервые представленном в Разделе 5.3.4:
it { should respond_to(:name) }
Такой вид тестов позволяет нам использовать TDD для добавления новых атрибутов и методов к нашей модели User и, в качестве побочного эффекта, мы получаем хорошую спецификацию методов на которые должны отвечать все объекты User.
Так как мы уже подготовили тестовую базу данных командой rake test:prepare, тесты должны пройти:
$ bundle exec rspec spec/
Возможно самой элементарной валидацией является валидация наличия, которая просто проверяет что данный атрибут имеется в наличии. Например, в данном разделе мы убеждаемся что оба name и email поля заполнены прежде чем пользователь будет сохранен в базе данных. В Разделе 7.3.3 мы увидим как распространить это требование на форму регистрации новых пользователей.
Мы начнем с теста на наличие атрибута name. Хотя первым шагом в TDD является написание провального теста (Раздел 3.2.1), в данном случае мы пока недостаточно знаем о валидации для того чтобы написать годный тест, так что мы вначале напишем валидацию и немного поиграем с ней в консоли, - чтобы побольше узнать о ней. Затем мы закомментируем валидацию, напишем провальный тест и проверим что раскомментирование валидации приводит к прохождению теста. Эта процедура может показаться излишне педантичной для такого простого теста, но я видел много "простых" тестов, которые на самом деле тестировали неправильные вещи; дотошность в TDD это просто единственный способ быть уверенными в том что мы тестируем правильные вещи. (Такая техника закомментирования также полезна при спасении приложения, код которого уже написан, но—quelle horreur! — (# какой ужас - фр.) не имеет тестов.)
Способ валидации наличия атрибута имени заключается в применении метода validates с аргументом presence: true, как это показано в Листинге 6.6. Аргумент presence: true это одноэлементный хэш опций; вспомните из Раздела 4.3.4 что фигурные скобки являются необязательными при передаче хеша в качестве последнего аргумента в методе. (Как отмечено в Разделе 5.1.1, использование хэшэй опций это очень распространенный прием в Rails.)
class User < ActiveRecord::Base validates :name, presence: true end
Листинг 6.6 возможно выглядит как магия, но validates это просто метод. Эквивалентная Листингу 6.6 формулировка с применением скобок выглядит следующим образом:
class User < ActiveRecord::Base validates :name, presence: true end
Давайте заскочим в консоль чтобы увидеть эффект добавления валидации к нашей модели User:User.new.
$ rails console --sandbox >> user = User.new(name: "", email: "mhartl@example.com") >> user.save => false >> user.valid? => false
Здесь user.save возвращает false, указывая на провальное сохранение. В заключительной команде мы используем valid? метод, который возвращает false когда объект приводит к сбою одной или более валидаций, и true когда все валидации проходят. В данном случае у нас есть только одна валидация, таким образом, мы знаем, какая именно провалилась, но все же не лишним будет в этом убедиться с помощью объекта errors, генерируемого при отказе:
>> user.errors.full_messages => ["Name can't be blank"]
(Сообщение об ошибке - подсказка, говорящая о том что Rails проверяет наличие атрибута, используя blank? метод, который мы видели в конце Раздела 4.4.3.)
Теперь о провальном тесте. Чтобы гарантировать что наш начальный тест перестанет работать, давайте закомментируем валидацию (Листинге 6.7).
class User < ActiveRecord::Base # validates :name, presence: true end
Начальный тест валидации представлен в Листинге 6.8.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should be_valid }
describe "when name is not present" do
before { @user.name = " " }
it { should_not be_valid }
end
end
Первый новый тест это просто проверка на то что объект @user изначально валиден:
it { should be_valid }
Это еще один пример булевой конвенции RSpec которую мы видели ранее в Разделе 6.2.1: в каждом случае, когда объект отвечает на булевый метод foo?, существует соответствующий тестовый метод с именем be_foo. В данном случае мы можем протестировать результат вызова
@user.valid?
с помощью
it "should be valid" do expect(@user).to be_valid end
Как и прежде, subject { @user } позволяет нам использовать однострочный стиль, что приводит к
it { should be_valid }
Второй тест вначале назначает пользовательскому имени недопустимое значение, а затем проверяет что получившийся объект @user невалиден:
describe "when name is not present" do
before { @user.name = " " }
it { should_not be_valid }
end
Здесь используется блок before для назначения невалидного значения атрибуту name, а затем происходит проверка того что получившийся объект user невалиден.
Теперь необходимо убедиться в том что в данный момент тесты провальны:
$ bundle exec rspec spec/models/user_spec.rb ...F 4 examples, 1 failure
Теперь раскомментируем валидацию (т.е., вернемся от Листинга 6.7 обратно к Листингу 6.6) для того чтобы получить прохождение теста:
$ bundle exec rspec spec/models/user_spec.rb .... 4 examples, 0 failures
Конечно, мы также хотим валидировать наличие адресов электронной почты. Тест (Листинг 6.9 ) походит на аналогичный тест для атрибута name.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when email is not present" do
before { @user.email = " " }
it { should_not be_valid }
end
end
Реализация практически та же, что и в Листинге 6.10.
class User < ActiveRecord::Base validates :name, presence: true validates :email, presence: true end
Теперь все тесты должны проходить и валидации "наличия" готовы.
Мы ограничили нашу модель User требованием имени для каждого пользователя, но мы должны пойти еще дальше: имена пользователей будут отображаться на сайте, таким образом, мы должны будем реализовать некоторое ограничение их длины. С работой, проделанной в Разделе 6.2.2, этот шаг легок.
Мы начнем с теста. В выборе максимальной длины нет ничего хитрого; мы просто примем 50 как разумную верхнюю границу, что означает что имена длиной в 51 символ будут слишком длинными (Листинг 6.11).
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when name is too long" do
before { @user.name = "a" * 51 }
it { should_not be_valid }
end
end
Для удобства мы использовали "мультипликацию строки" в Листинге 6.11 для создания строки длиной в 51 символ. Мы можем увидеть как это работает, используя консоль:
>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51
Тест в Листинге 6.11 должен провалиться. Чтобы заставить его пройти, мы должны знать об аргументе валидации, ограничивающим длину, :length, наряду с :maximum параметром реализуют верхнюю границу (Листинг 6.12).
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true
end
С нашим комплектом тестов, вновь проходящим, мы можем идти дальше, к более интересной валидации: валидации формата электронной почты.
Наши валидации для атрибута name реализуют только минимальные ограничения: любое непустое имя длиной до 51 символов пройдет; но, конечно, атрибут email должен соответствовать более строгим требованиям. До сих пор мы отклоняли только пустой адрес электронной почты; в этом разделе мы потребуем, чтобы адреса электронной почты соответствовали знакомому образцу user@example.com.
Ни тесты, ни валидации не будут исчерпывающими - лишь достаточно хорошими, чтобы принять большую часть допустимых адресов электронной почты и отклонить большинство недопустимых. Мы начнем с пары тестов, включающих наборы допустимых и недопустимых адресов. Чтобы сделать эти наборы, стоит узнать о полезной технике создания массивов строк, как показано в этом консольном сеансе:
>> %w[foo bar baz] => ["foo", "bar", "baz"] >> addresses = %w[user@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp] => ["user@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"] >> addresses.each do |address| ?> puts address >> end user@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp
Здесь мы выполнили итерации по элементам массива addresses используя each метод (Раздел 4.3.2). Вооружившись этой техникой мы готовы написать несколько базовых тестов для валидации формата электронной почты (Листинге 6.13).
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when email format is invalid" do
it "should be invalid" do
addresses = %w[user@foo,com user_at_foo.org example.user@foo.
foo@bar_baz.com foo@bar+baz.com]
addresses.each do |invalid_address|
@user.email = invalid_address
expect(@user).not_to be_valid
end
end
end
describe "when email format is valid" do
it "should be valid" do
addresses = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
addresses.each do |valid_address|
@user.email = valid_address
expect(@user).to be_valid
end
end
end
end
Как было отмечено выше, они не являются исчерпывающими, но мы проверили обычные допустимые формы электронной почты user@foo.COM, THE_US-ER@foo.bar.org (верхний регистр, подчеркивание и соединенные домены) и first.last@foo.jp (стандартное корпоративное имя пользователя first.last, с двухбуквенным доменом верхнего уровня jp (Japan)), наряду с несколькими недопустимыми формами.
Код приложения для валидации формата электронной почты использует регулярное выражение (или regex) для определения формата, наряду с :format аргументом для validates метода (Листинге 6.14).
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
end
Здесь регулярное выражение VALID_EMAIL_REGEX это константа, которая обозначается в Ruby именем начинающимся с большой буквы. Код
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
обеспечивает допустимость адресов электронной почты соответствующих образцу, все остальные будут считаться недопустимыми.
Так, откуда появился образец? Регулярные выражения состоят из краткого (некоторые сказали бы нечитаемого) языка для сравнения текстовых паттернов; изучение построения регулярных выражений это искусство и для начала я разбил VALID_EMAIL_REGEX на небольшие куски (рис 6.1
| /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i | полное регулярное выражение |
|---|---|
| / | начало регулярного выражения |
| \A | соответствует началу строки |
| [\w+\-.]+ | по крайней мере один символ слова, плюс, дефис или точка |
| @ | буквально "знак собаки" |
| [a-z\d\-.]+ | по крайней мере одна буква, цифра, дефис или точка |
| \. | буквальная точка |
| [a-z]+ | по крайней мере одна буква |
| \z | соответствует концу строки |
| / | конец регулярного выражения |
| i | нечувствительность к регистру |
Кстати, на самом деле существует полное регулярное выражение для сопоставления адресов электронной почты в соответствии с официальным стандартом, но волноваться не стоит. Экземпляр из Листинга 6.14 тоже хорош, возможно даже лучше чем официальный.
(рис 6.4) Удивительный редактор регулярных выражений Rubular
Теперь тесты должны пройти. (Фактически, тесты для валидных адресов электронной почты должны были проходить все время; так как регулярные выражения, как известно, подвержены ошибкам, действительные испытания электронной почты в основном заключаются в санитарной проверке на VALID_EMAIL_REGEX.) Это означает, что осталось рассмотреть только одно ограничение: обеспечение уникальности адресов электронной почты.
Для обеспечения уникальность адресов электронной почты (так, чтобы мы могли использовать их в качестве имен пользователей) мы будем использовать :unique опцию для validates метода. Но предупреждаю: есть важное предостережение, так что не просто просмотрите раздел, а прочитайте его внимательно.
Мы начнем, как обычно, с наших тестов. В наших предыдущих тестах модели мы, главным образом, использовали User.new, который только создает объект Ruby в памяти, но для тестов уникальности мы фактически должны поместить запись в базу данных.14 (Первый) тест дублирования электронной почты представлен в Листинге 6.15.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
subject { @user }
.
.
.
describe "when email address is already taken" do
before do
user_with_same_email = @user.dup
user_with_same_email.save
end
it { should_not be_valid }
end
end
Методика заключается в создании пользователя с тем же адресом электронной почты, что и у @user, чего мы достигаем с помощью @user.dup, который создает дубликат пользователя с теми же атрибутами. Поскольку мы затем сохраняем этого пользователя, оригинальный @user будет иметь адрес электронной почты который уже существует в базе данных и, следовательно, он не должен быть валидным.
Мы можем получить прохождение теста из Листинга 6.15 с кодом из Листинге 6.16.
class User < ActiveRecord::Base
.
.
.
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: true
end
И все же мы не закончили. Адреса электронной почты не чувствительны к регистру —
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
subject { @user }
.
.
.
describe "when email address is already taken" do
before do
user_with_same_email = @user.dup
user_with_same_email.email = @user.email.upcase
user_with_same_email.save
end
it { should_not be_valid }
end
end
Здесь мы используем upcase метод на строках (описан кратко в Разделе 4.3.2). Этот тест делает то же самое что и первый тест на дублирование адресов электронной почты, но с прописным адресом электронной почты. Если этот тест кажется вам немного абстрактным, запустите консоль:
$ rails console --sandbox >> user = User.create(name: "Example User", email: "user@example.com") >> user.email.upcase => "USER@EXAMPLE.COM" >> user_with_same_email = user.dup >> user_with_same_email.email = user.email.upcase >> user_with_same_email.valid? => true
Конечно, сейчас user_with_same_email.valid? является true, так как это провальный тест, но мы хотим, чтобы оно было false. К счастью, :uniqueness принимает опцию, :case_sensitive, как раз для этой цели (Листинг 6.18).
class User < ActiveRecord::Base
.
.
.
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
end
Обратите внимание: мы просто заменили true на case_sensitive: false; Rails в этом случае делает вывод, что :uniqueness должно быть true. В этой точке наше приложение обеспечивает уникальность адресов электронной почты и наш набор тестов должен пройти.
Предостережение уникальности
Есть одна небольшая проблема, предостережение, на которое я ссылался выше:
Использование validates :uniqueness не гарантирует уникальности.
D’oh! Но что может может пойти не так? А вот что:
Если вышеописанная последовательность кажется вам неправдоподобной, поверьте мне, это не так: это происходит на любом Rails сайте со значительным трафиком. К счастью, решение просто в реализации, нам просто необходимо обеспечить уникальность также на уровне базы данных. Наш метод заключается в создании в базе данных индекса столбца электронной почты и последующем требовании уникальности этого индекса.
Индекс адреса электронной почты представляет собой обновление требований к нашей модели данных что (как обсуждалось в Разделе 6.1.1) делается в Rails посредством миграций. Мы видели в Разделе 6.1.1 что генерация модели User автоматически создает новую миграцию (Листинг 6.2); в данном случае мы добавляем структуру к существующей модели, таким образом, мы должны создать миграцию непосредственно, используя migration генератор:
$ rails generate migration add_index_to_users_email
В отличие от миграции для пользователей, миграция уникальности электронной почты не предопределена, таким образом, мы должны заполнить ее содержание кодом из Листинга 6.19.
class AddIndexToUsersEmail < ActiveRecord::Migration
def change
add_index :users, :email, unique: true
end
end
Здесь используется Rails метод add_index для добавления индекса на столбце email таблицы users. Индекс сам по себе не обеспечивает уникальность, но это делает опция unique: true.
Заключительный шаг должен мигрировать базу данных:
$ bundle exec rake db:migrate
(Если это не сработало, попробуйте закрыть все консольные сессии в песочнице, которая может блокировать базу данных, тем самым препятствуя миграции.) Если вам интересно посмотреть на практический результат выполнения этой команды, посмотрите файл db/schema.rb, который теперь должен содержать строку подобную этой:
add_index "users", ["email"], name: "index_users_on_email", unique: true
К сожалению, есть еще одно изменение которое мы должны сделать для того чтобы быть уверенными в уникальности email адресов - все email адреса должны быть в нижнем регистре, прежде чем они будут сохранены в базе данных. Причина заключается в том что не все адаптеры баз данных используют регистрозависимые индексы.before_save для того чтобы принудить Rails переводить в нижний регистр email атрибут перед сохранением пользователя в базу данных, как это показано в Листинге
6.20.
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
.
.
.
end
Код в Листинге 6.20 передает блок в коллбэк before_save и назначает email адрес пользователя равным его текущему значению в нижнем регистре с помощью метода строки downcase. Этот код довольно продвинутый и в этой точке я советую вам просто поверить в то что он работает; если вы все же сомневаетесь, закомментируйте валидацию уникальности из Листинга 6.16 и попробуйте создать пользователей с идентичными email адресами для того чтобы посмотреть на результирующую ошибку. (Мы вновь увидим эту технику в Разделе 8.2.1, где мы будем использовать конвенцию method reference.) Написание теста для кода в Листинге 6.20 остается в качестве упражнения (Раздел 6.5).
Теперь вышеописанный сценарий с Алисой будет хорошо работать: база данных сохранит запись пользователя, основанную на первом запросе, и отвергнет второе сохранение за нарушение уникальности. (Ошибка появится в логе Rails, но в этом нет ничего плохого. Можно даже отловить ActiveRecord::StatementInvalid исключение, но в этом учебном руководстве мы не будем заморачиваться этим шагом.) Добавление этого индекса на атрибут адреса электронной почты преследует вторую цель, кратко рассмотренную в Разделе 6.1.4: он решает проблему эффективности поиска пользователя с помощью find_by (Блок 6.2).
_________________________________________________________
Блок 6.2.Индексы базы данных
При создании столбца в базе данных, важно учитывать, что нам нужно будет найти записи по этому столбцу. Рассмотрим, например, email атрибут созданый миграцией в Листинге 6.2. Когда мы позволим пользователям регистрироваться на сайте, начиная с Главы 7, нам нужно будет иметь возможность находить запись пользователя соответствующую предоставленному адресу электронной почты; в базе данных, основанной, к сожалению, на наивной модели данных, единственный способ найти пользователя по его адресу электронной почты, это просмотреть строку каждого пользователя в базе данных и сравнить его адрес электронной почты с атрибутом предоставленного адреса электронной почты. Это известно в бизнесе баз данных, как full-table scan (полное сканирование таблицы), а для реального сайта с тысячами пользователей это Bad Thing.
Добавление индекса к столбцу электронной почты решает проблему. Чтобы понять индекс базы данных, полезно рассмотреть аналогию с индексом книги. В книге, чтобы найти все вхождения заданной строки, например "foobar", вам придется сканировать каждую страницу в поиске "foobar". С индексом книги, с другой стороны, вы можете просто посмотреть "foobar" в индексе, чтобы увидеть все страницы, содержащие "foobar". Индекс базы данных работает, по сути, аналогичным образом.
_________________________________________________________
В этом разделе мы добавим последний из базовых атрибутов модели User: безопасный пароль используемый для аутентификации пользователей примера приложения. Методика заключается в запросе у каждого пользователя пароля (с подтверждением), а затем сохранении зашифрованной версии пароля в базе данных. Мы также добавим способ аутентификации пользователя опирающийся на данный пароль, метод, который мы будем использовать в Главе 8 для того чтобы дать пользователям возможность входить на сайт.
Метод для аутентификации будет принимать введеный пароль, шифровать его и сравнивать зашифрованное значение с хранящимся в базе данных. Если они совпадают, то введеный пароль корректен и пользователь аутентифицирован. Сравнивая зашифрованные пароли вместо того чтобы сравнивать пароли непосредственно, мы получаем возможность аутентифицировать пользователей не храня в базе данных сами пароли, тем самым избежав серьезной дыры в системе безопасности нашего приложения.
БОльшая часть механики безопасного пароля будет реализована с помощьо одного Rails-метода называемого has_secure_password (впервые был представлен в Rails 3.1). Поскольку очень многое в дальнейшем зависит от этого единственного метода, трудно разрабатывать безопасные пароли постепенно. Начиная с Раздела 6.3.2 я рекомендую добавить метод has_secure_password заранее, а затем комментировать его перед добавлением каждого нового теста для того чтобы обеспечить правильный цикл TDD. (Поскольку скринкасты позволяют демонстрировать более постепенный подход к разработке, заинтересованным читателям следует посмотреть Ruby on Rails Tutorial screencasts для более полного понимания этого материала.)
Мы начнем с необходимого изменения модели данных для пользователей, что подразумевает добавление ). Название digest пришло из терминологии криптографических хэш функций, а само имя password_digest необходимо для работы реализации в Разделе 6.3.4. Как следует зашифровав пароль, мы обеспечим невозможность получения доступа к сайту атакером, даже если он умудрится получить копию базы данных.
(рис 6.5) Модель User с добавленным атрибутом password_digest.
Мы будем использовать самую новомодную хэш функцию, называемую bcrypt для необратимого шифрования пароля в виде хэша пароля. Для того чтобы использовать bcrypt в примере приложения нам необходимо добавить гем bcrypt-ruby в наш Gemfile (Листинг 6.21).
source 'https://rubygems.org' ruby '2.0.0' #ruby-gemset=railstutorial_rails_4_0 gem 'rails', '4.0.2' gem 'bootstrap-sass', '2.3.2.0' gem 'bcrypt-ruby', '3.1.2' . . .
Затем запускаем bundle install:
$ bundle install
Поскольку мы хотим чтобы пользователи имели столбец password digest, объект user должен отвечать на password_digest, что приводит нас к тесту показанному в Листинге 6.22.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
.
.
.
end
Для того чтобы получить прохождение этого теста мы вначале генерируем соответствующую миграцию для столбца password_digest:
$ rails generate migration add_password_digest_to_users password_digest:string
Здесь первый аргумент это название миграции и мы также предоставили второй аргумент с названием и типом атрибута который мы хотим создать. (Сравните это с начальной генерацией таблицы users в Листинге 6.1.) Мы можем выбрать любое название для миграции, но было бы удобно, если бы ее название заканчивалось на _to_users, поскольку в этом случае Rails автоматически построит миграцию для добавления столбцов к таблице users. Кроме того, включив второй аргумент, мы дали Rails достаточно информации для построения для нас всей миграции, как это видно в Листинге 6.23.
class AddPasswordDigestToUsers < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
end
end
Этот код использует метод add_column для добавления столбца password_digest к таблице users.
Мы можем получить прохождение провального теста из Листинга 6.22 запустив миграцию базы данных разработки и подготовив тестовую базу данных:
$ bundle exec rake db:migrate $ bundle exec rake test:prepare $ bundle exec rspec spec/
Как видно на наброске рис 6.1, мы ожидаем что пользователи должны будут подтверждать свои пароли, что является общепринятой в сети практикой минимизирующей риск опечаток при введении пароля. Мы можем реализовать это на уровне контроллера, но принято делать это в модели и использовать Active Record для наложения этого ограничения. Метод заключается в добавлении password и password_confirmation атрибутов к модели User и последующем требовании совпадения этих двух атрибутов перед сохранением записи в базе данных. В отличие от всех остальных атрибутов, что мы видели до этого, атрибуты пароля будут виртуальными — они будут лишь временно существовать в памяти и не будут постоянно храниться в базе данных. Как мы увидим в Разделе 6.3.4, has_secure_password реализует эти виртуальные атрибуты автоматически .
Мы начнем с respond_to тестов для пароля и его подтверждения, как это показано в Листинге 6.24.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should be_valid }
.
.
.
end
обратите внимание - мы добавили :password и :password_confirmation в хэш инициализации для User.new:
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
Мы определенно не хотим чтобы пользователи могли вводить пустые пароли, так что мы добавим еще один тест для валидации наличия пароля:
describe "when password is not present" do
before { @user.password = @user.password_confirmation = " " }
it { should_not be_valid }
end
Поскольку мы будем вскоре тестировать несовпадение пароля, здесь мы будем тестировать существование валидации наличия установив пароль и его подтверждение равными чистой строке.
Мы также хотим убедиться что пароль и его подтверждение совпадают. Случай, когда они совпадают покрыт с помощью it { should be_valid }, так что нам осталось протестировать только случай несовпадения:
describe "when password doesn't match confirmation" do
before { @user.password_confirmation = "mismatch" }
it { should_not be_valid }
end
Собрав все вместе, мы получаем (провальные) тесты в Листинге 6.25.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should be_valid }
.
.
.
describe "when password is not present" do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: " ", password_confirmation: " ")
end
it { should_not be_valid }
end
describe "when password doesn't match confirmation" do
before { @user.password_confirmation = "mismatch" }
it { should_not be_valid }
end
end
Мы можем получить прохождение тестов из Листинга 6.25 используя лишь одну строку кода - как это показано в Листинге 6.26.
class User < ActiveRecord::Base . . . has_secure_password end
Примечательно что одна линия
has_secure_password
приводит к прохождению всех текущих тестов пароля. На самом деле она делает гораздо больше, даже слишком много, мешая будущим тестам краснеть перед позеленением, так что прежде чем двигаться дальше я рекомендую закоментировать эту строку (Листинге 6.27).
class User < ActiveRecord::Base . . . # has_secure_password end
Последняя часть механики наших паролей это метод для получения пользователей по их email и паролям. Эта задача естественным образом разбивается на две части: первая из них это поиск пользователя по адресу электронной почты; вторая это аутентификация пользователя с данным паролем. Все тесты (кроме последнего) в этом разделе связаны с has_secure_password, так что в процессе реализации вы должны иметь возможность раскомментировать закомментированную строку из Листинге 6.27 для того чтобы дать тестам возможность пройти.
Первый шаг прост; как мы видели в Разделе 6.1.4, мы можем найти пользователя с данным адресом электронной почты с помощью метода find_by:
user = User.find_by(email: email)
Второй шаг заключается в применении метода authenticate для проверки того что у пользователя есть данный пароль. В Главе 8, мы будем получать текущего (вошедшего) пользователя используя код вроде этого:
current_user = user.authenticate(password)
Если данный пароль совпадает с паролем пользователя, он должен вернуть пользователя; в противном случае он должен вернуть false.
Как обычно, мы можем выразить требования для authenticate используя RSpec. Получившиеся в результате тесты являются немного более продвинутыми чем те что мы видели до этого, так что давайте разобьем их на части; если вы новичок в RSpec, вам возможно понадобится прочитать этот раздел несколько раз. Мы начнем с того, что объект User должен отвечать на authenticate:
it { should respond_to(:authenticate) }
Затем мы покрываем два случая - совпадение и несовпадения пароля:
describe "return value of authenticate method" do
before { @user.save }
let(:found_user) { User.find_by(email: @user.email) }
describe "with valid password" do
it { should eq found_user.authenticate(@user.password) }
end
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not eq user_for_invalid_password }
specify { expect(user_for_invalid_password).to be_false }
end
end
Блок before сохраняет пользователя в базе данных, так что он может быть получен с помощью find_by, чего мы достигаем используя let method:
let(:found_user) { User.find_by(email: @user.email) }
Мы уже использовали let в нескольких упражнениях, это первый случай когда мы его видим в основном тексте учебника. Блок 6.3 рассказывает о let более подробно.
Два блока describe покрывают случаи когда @user и found_user должны быть одинаковыми (совпадение пароля) и разными (несовпадение пароля); они используют "равенство" eq для проверки эквивалентности объектов (который в свою очередь использует ==, как мы видели в Разделе 4.3.1). Обратите внимание что тесты в
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not eq user_for_invalid_password }
specify { expect(user_for_invalid_password).to be_false }
end
используют let второй раз, а также используют метод specify. Это просто синоним для it, который может быть использован когда it звучит ненатурально
___________________________________________
Блок 6.3.Применение let
RSpec метод let обеспечивает удобный способ для создания локальных переменных внутри тестов. Синтаксис может выглядеть немного странно, но его эффект аналогичен назначению переменной. Аргументом let является символ и он принимает блок, который возвращает значение назначаемое локальной переменной с именем символа. Другими словами,
let(:found_user) { User.find_by(email: @user.email) }
создает переменную found_user чье значение эквивалентно результату find_by. Затем мы можем использовать эту переменную в любом before или it блоке на протяжении всего оставшегося теста. Одно из достоинств let заключается в том, что он мемоизирует свое значение, это означает что он запоминает значение от одного вызова до другого. (Обратите внимание, что memoize это технический термин; в частности, его не надо путать с "memorize".) В данном случае, поскольку let мемоизирует переменную found_user, метод find_by будет вызван лишь единожды при запуске спеков модели User.
___________________________________________
Наконец, в качестве дополнительной меры предосторожности, мы протестируем на наличие валидации длины паролей, установив длину паролей не меньшей чем шесть знаков:
describe "with a password that's too short" do
before { @user.password = @user.password_confirmation = "a" * 5 }
it { should be_invalid }
end
Собрав вместе все тесты мы получаем Листинге 6.28.
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
.
.
.
it { should respond_to(:authenticate) }
.
.
.
describe "with a password that's too short" do
before { @user.password = @user.password_confirmation = "a" * 5 }
it { should be_invalid }
end
describe "return value of authenticate method" do
before { @user.save }
let(:found_user) { User.find_by(email: @user.email) }
describe "with valid password" do
it { should eq found_user.authenticate(@user.password) }
end
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not eq user_for_invalid_password }
specify { expect(user_for_invalid_password).to be_false }
end
end
end
Как было отмечено в Блоке 6.3, let мемоизирует свое значение, так что первый вложенный describe блок в Листинге 6.28 вызывает let для получения пользователя из базы данных с помощью find_by, но второй describe блок уже не обращается к базе данных.
В предыдущей версии Rails, добавление безопасного пароля было сложным и долгим, как это можно увидеть в http://railstutorial.ru/chapters/3_0/beginning
Во-первых, нам нужна валидация длины для пароля которая использует ключ :minimum по аналогии с ключом :maximum из Листинга 6.12:
validates :password, length: { minimum: 6 }
(Валидации наличия и подтверждения автоматически добавляются has_secure_password.)
Во-вторых, нам нужно добавить к атрибутам password и password_confirmation требование наличия пароля, требование их совпадения и добавить authenticate метод для сравнения зашифрованного пароля с password_digest для аутентификации пользователей. Это единственный непростой шаг и в последней версии Rails все эти фичи бесплатно поставляются в одном методе - has_secure_password:
has_secure_password
Пока столбец password_digest присутствует в базе данных, добавление одного лишь этого метода к нашей модели дает нам безопасный способ для создания и аутентификации новых пользователей.
(Если вы хотите увидеть как реализован has_secure_password, я советую взглянуть на хорошо документированый и вполне читабельный исходный код secure_password.rb. Этот код включает строки
validates_confirmation_of :password,
if: lambda { |m| m.password.present? }
которые (как описано в Rails API) автомагически создают атрибут password_confirmation. Он также включает валидацию для password_digest атрибута.
Совместно с валидацией наличия из Листинга 6.26, вышеприведенные элементы приводят к модели User показаной в Листинге 6.29, который завершает реализацию безопасных паролей.
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true,
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
end
Теперь необходимо убедиться что набор тестов проходит:
$ bundle exec rspec spec/
Примечание: Если вы получили deprecation warning вроде
[deprecated] I18n.enforce_available_locales will default to true in the future
вы можете попробовать избавиться от него отредактировав config/application.rb следующим образом:
require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
class Application < Rails::Application
.
.
.
I18n.enforce_available_locales = true
.
.
.
end
end
Теперь, когда базовая модель User завершена, мы создадим пользователя в базе данных в качестве подготовки к созданию страницы показывающей информацию о пользователе в Разделе 7.1. Это также даст нам шанс сделать работу, проделанную в предыдущих разделах, более ощутимой; одни лишь проходяшие тесты могут показаться разочаровывающим результатом - гораздо приятнее увидеть настоящую запись пользователя в базе данных.
Поскольку мы пока не можем зарегистрироваться через веб-интерфейс — это является целью Главы 7 — мы будем использовать Rails консоль для создания нового пользователя вручную. В отличие от Раздела 6.1.3, в этом разделе мы не должны работать в песочнице, поскольку в этот раз нашей целью является сохранение записи в базе данных:
$ rails console >> User.create(name: "Michael Hartl", email: "mhartl@example.com", ?> password: "foobar", password_confirmation: "foobar") => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2013-03-11 20:45:19", updated_at: "2013-03-11 20:45:19", password_digest: "$2a$10$kn4cQDJTzV76ZgDxOWk6Je9A0Ttn5sKNaGTEmT0jU7.n...">
Для того чтобы проверить что это работает, давайте взглянем на строку в девелопмент базе данных (). Обратите внимание, что столбцы соответствуют атрибутам модели данных, определенной на рис 6.5.
(рис 6.6) Строка пользователя в базе данных SQLite db/development.sqlite3.
Вернувшись в консоль, мы можем увидеть эффект has_secure_password из Листинга 6.29 посмотрев на атрибут password_digest:
>> user = User.find_by(email: "mhartl@example.com") >> user.password_digest => "$2a$10$kn4cQDJTzV76ZgDxOWk6Je9A0Ttn5sKNaGTEmT0jU7.ncBJ/60gHq"
Это зашифрованная версия пароля ("foobar") которая была использована для инициализации объекта user. Мы также можем проверить что команда authenticate работает, использовав вначале невалидный, а затем валидный пароли:
>> user.authenticate("invalid")
=> false
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2013-03-11 20:45:19", updated_at: "2013-03-11 20:45:19",
password_digest: "$2a$10$kn4cQDJTzV76ZgDxOWk6Je9A0Ttn5sKNaGTEmT0jU7.n...">
Как и требовалось тестами в Листинге 6.28, authenticate возвращает false если пароль невалиден и самого пользователя в противном случае.
Начав с нуля, в этой главе мы создали рабочую модель User с name, email и различными "парольными" атрибутами, вместе с валидациями обеспечивающими несколько важных ограничений на их значения. Кроме того, мы можем безопасно аутентифицировать пользователей с помощью пароля. В предыдущих версиях Rails такой подвиг потребовал бы в два раза большего количества кода, но благодаря компактному методу validates и has_secure_password, мы смогли построить рабочую модель User используя меньше дюжины строк исходного кода.
В следующей (седьмой) главе мы сделаем рабочую форму регистрации для создания новых пользователей, вместе со страницей для отображения информации о каждом пользователе. В Главе 8 мы будем использовать механизм аутентификации из Раздела 6.3 для того чтобы дать пользователям возможность входить на сайт.
Если вы используете Git, было бы неплохо закоммитить внесенные изменения, если вы этого еще не сделали:
$ git add . $ git commit -m "Make a basic User model (including secure passwords)"
Затем опять объединить их с мастер веткой:
$ git checkout master $ git merge modeling-users
reload для перезагрузки значения из базы данных и метод eq для тестирования на равенство. Закомменировав строку before_save убедитесь что тесты из Листинга 6.30 тестируют правильные вещи.before_save может быть написан способом показанным в Листинге 6.31.ActiveRecord::Base для того чтобы получить представление о ее возможностях.validates для того чтобы больше узнать о его возможностях и опциях.require 'spec_helper'
describe User do
.
.
.
describe "email address with mixed case" do
let(:mixed_case_email) { "Foo@ExAMPle.CoM" }
it "should be saved as all lower-case" do
@user.email = mixed_case_email
@user.save
expect(@user.reload.email).to eq mixed_case_email.downcase
end
end
.
.
.
end
class User < ActiveRecord::Base
has_secure_password
before_save { email.downcase! }
.
.
.
end
class User < ActiveRecord::Base
.
.
.
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
.
.
.
end
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.