В части 5 мы представим еще два маршрута для учетных записей и транзакций, чтобы можно было редактировать эти ресурсы. Это даст возможность использовать удобный вспомогательный метод before_action, который выполняется перед указанными действиями. Затем мы представим владельцев учетных записей, помня о том, что учетные записи и владельцы являются ассоциацией «многие ко многим».

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

Редактирование ресурсов

Редактировать учетные записи. Ранее объяснялось, что формы на основе модели очень полезны, поскольку их можно использовать как для создания нового ресурса, так и для обновления существующего ресурса. Большая часть работы уже сделана. Начните с предоставления маршрутов :edit и :update в список открытых ресурсов учетных записей.

resources :accounts, only: [:index, :create, :new, :show, :edit, :update] do
...

Теперь есть только один маршрут для действий CRUD на учетных записях, которые мы не раскрывали, :destroy. Возможно, имеет смысл снова изменить эту строку на:

resources :accounts, except: [:destroy] do
...

Маршрут открыт, теперь создайте действие редактирования. Этому действию потребуется объект учетной записи для заполнения формы, которую мы напишем в представлении редактирования.

def edit
  @account = Account.find params[:id]
end

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

<%= render 'shared/content_title', title: "Edit Account: #{@account.name}" %>
<div class="space">
  <%= link_to "Back to Account", account_path(@account), class: "btn btn-main" %>
</div>
<%= render 'shared/errors', obj: @account %>
<%= form_for @account do |f| %>
  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name, class: 'form-control' %>
  </div>
  <div class="form-group">
    <%= f.label :kind, 'Type' %>
    <%= f.select(:kind, Account::KINDS.map { |k| [k, k.downcase] }, {}, { class: 'form-control' }) %>
  </div>
  <%= f.submit 'Update Account', class: 'btn btn-success' %>
<% end %>

Было сделано всего несколько небольших изменений, чтобы адаптировать код из нового для редактирования. На самом деле, сама форма не была изменена вообще. Теперь очевидным действием является извлечение формы в партиал и рендеринг этого партиала в представлениях new и edit.

Создайте _form.html.erb в папке представления учетных записей.

<%= form_for account do |f| %>
  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name, class: 'form-control' %>
  </div>
  <div class="form-group">
    <%= f.label :kind, 'Type' %>
    <%= f.select(:kind, Account::KINDS.map { |k| [k, k.downcase] }, {}, { class: 'form-control' }) %>
  </div>
  <%= f.submit((account.new_record? ? 'Create' : 'Update') + ' Account' , class: 'btn btn-success') %>
<% end %>

Единственное изменение, которое нам нужно сделать, это удалить символ «@» перед «учетными записями», потому что это больше не переменная экземпляра (также немного логики, чтобы изменить то, что говорит кнопка отправки). Мы передадим объект учетной записи в метод рендеринга.

В представлениях new и edit замените код формы этой единственной строкой.

<%= render 'form', account: @account %>

Убедитесь, что форма все еще работает в представлении new, затем мы напишем код для действия update и протестируем форму в представлении edit.

Вернитесь к контроллеру учетных записей и создайте действие update. Эта логика аналогична логике действия create, но мы используем метод update вместо метода save; он делает то, что вы думаете, и попадает в базу данных, возвращая true в случае успеха и false в противном случае.

def update
  @account = Account.find params[:id]
  if @account.update(account_params)
    flash[:notice] = "Your account was successfully updated."
    redirect_to account_path(@account)
  else
    render :edit
  end
end

Проверьте форму редактирования, перейдя на /accounts/1/edit, убедитесь, что у вас есть хотя бы одна учетная запись. Мы также можем создать ссылку на страницу редактирования, используя вспомогательный метод edit_account_path и передав объект учетной записи.

<%= link_to 'Edit Account', edit_account_path(@account), class: 'btn btn-primary' %>

Редактировать транзакции —То же самое здесь. Сначала выставьте правильные маршруты.

resources :accounts, except: [:destroy] do
  resources :transactions, only: [:new, :create, :edit, :update]
end

Создайте представление edit в папке транзакций. Скопируйте и вставьте в него код из представления new. Создайте новый файл _form.html.erb, но на этот раз в папке транзакций. Вырежьте код формы из редактирования или нового представления и вставьте, а затем немного измените его.

<%= form_for [account, transaction] do |f| %>
  <div class="form-group">
    <%= f.label :kind, 'Type' %>
    <%= f.select(:kind, Transaction::KINDS.map { |k| [k, k.downcase] }, {}, { class: 'form-control' }) %>
  </div>
  <div class="form-group">
    <%= f.label :amount %>
    <%= f.text_field :amount, value: display_amount(transaction.amount), class: 'form-control' %>
  </div>
  <div class="form-group">
    <%= f.label :description %>
    <%= f.text_area :description, class: 'form-control' %>
  </div>
  <%= f.submit((transaction.new_record? ? 'Create' : 'Update') + ' Transaction', class: 'btn btn-success') %>
<% end %>

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

<%= render 'form', account: @account, transaction: @transaction %>

Не забудьте также изменить заголовок на странице для режима редактирования. Мы также должны разместить ссылку на представление show для каждой транзакции, которую пользователь может использовать для ее редактирования. Добавьте следующее в блок each для транзакций:

<td>
  <%= link_to 'Edit', edit_account_transaction_path(@account, trans), class: "btn btn-primary" %>
</td>

У нас есть маршрут и вид, остался только контроллер. Перейдите к контроллеру транзакций и добавьте следующие два метода.

def edit  
  @account = Account.find params[:account_id]
  @transaction = Transaction.find params[:id]
end
def update
  @account = Account.find params[:account_id]
  @transaction = Transaction.find params[:id]
  if @transaction.update(transaction_params)
    flash[:notice] = "Your transaction was successfully updated."
    redirect_to account_path(@account)
  else
    render :edit
  end
end

Метод edit очень похож на метод new, за исключением того, что теперь у нас есть транзакция для выборки. Метод update очень похож на метод create, за исключением того, что метод обновления используется для транзакции вместо сохранения, а представление редактирования отображается при возникновении ошибки.

Проверьте, все должно пройти по плану.

Перед действиями

Давайте отступим на секунду и немного приведем в порядок. В Rails есть вспомогательный метод before_action, который, как вы могли догадаться, выполняет указанный код перед списком указанных действий. Его использование — отличный способ консолидировать код и сэкономить время. Мы будем использовать его в простейшей форме, предоставив ему метод, который мы хотим запускать перед определенными действиями, и параметр :only или :except, указывающий, какие действия.

Взгляните на AccountsController, и вы заметите несколько действий, которые настраивают объект учетной записи. Извлеките это в метод, а затем вызовите его в действии перед. Создайте частный метод, который настроит объект учетной записи.

def set_account
  @account = Account.find params[:id]
end

Затем установите before_action в верхней части класса.

class AccountsController < ApplicationController
  before_action :set_account, only: [:show, :edit, :update]
  #... rest of class ...

Теперь удалите строку в действиях show, edit и update, которая устанавливает объект учетной записи. Это должно оставить действия show и edit пустыми. Протестируйте приложение и убедитесь, что вы по-прежнему можете получать доступ к учетной записи и редактировать ее.

Мы можем следовать аналогичному процессу, чтобы установить учетную запись и объекты транзакций в контроллере транзакций. Однако на этот раз нам нужно настроить учетную запись на четыре действия и транзакцию только на два действия.

class TransactionsController < ApplicationController
  before_action :set_account, only: [:new, :create, :edit, :update]
  before_action :set_transaction, only: [:edit, :update]
  #... rest of class ...

Конечно, создайте закрытые методы set_account и set_transaction внутри класса. Снова проверьте приложение и убедитесь, что ничего не изменилось.

Создание держателей и аутентификация

До сих пор мы имели дело только с двумя из трех моделей, которые мы установили еще в первом посте. Рекомендуется создавать приложение по частям, часто тестируя, чтобы гарантировать, что проблемы будут обнаружены как можно скорее. Гораздо проще найти источник проблемы в 20 строках кода, чем в 200. Имейте это в виду при создании приложений; завершите свой ERD как можно лучше, но не пытайтесь кодировать все сразу, иначе вы никогда не сможете найти эти неизбежные ошибки.

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

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

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

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

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

По соглашению Rails столбец, который нам нужен в таблице держателей в базе данных, равен password_digest, так как это не фактический пароль, а дайджест, созданный на его основе. Создайте новую миграцию Rails с помощью rails generate migration add_password_digest_to_holders.

В этом файле добавьте add_column :holders, :password_digest, :string к методу change, затем запустите rake db:migrate в терминале, чтобы изменения попали в базу данных. Теперь мы можем создать страницу регистрации/входа.

Настройте маршруты, которые нам понадобятся. Под root to: 'accounts#index' в файле маршрутов добавьте:

get '/register', to: 'holders#new', as: 'register'
resources :holders, only: [:create, :show, :edit, :update]

Нам нужен собственный именованный путь для действия new, назвав его просто /register вместо /holders/new. Остальные ресурсы создаются как обычно.

Создайте файл holders_controller.rb и добавьте следующее:

def new
  @holder = Holder.new
end

Теперь создайте файл new.html.erb в папке app/views/holders. На данный момент этот файл будет довольно простым.

<%= render 'shared/content_title', title: 'Register' %>
<%= render 'shared/errors', obj: @holder %>
<%= render 'form' %>

Мы получили бы ошибку, если бы попытались получить доступ к странице holders/new, потому что мы еще не создали форму. Создайте новую форму как app/views/holders/_form.html.erb.

<%= form_for @holder do |f| %>
  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name, class: 'form-control' %>
  </div>
  <div class="form-group">
    <%= f.label :password %>
    <%= f.password_field :password, class: 'form-control' %>
  </div>
  <div class="form-group">
    <%= f.label :password_confirmation, 'Confirm Password' %>
    <%= f.password_field :password_confirmation, class: 'form-control' %>
  </div>
  <%= f.submit "Register", class: 'btn btn-success' %>
<% end %>

Здесь следует отметить пару моментов:

  1. Мы можем использовать объект holder непосредственно в форме, потому что он никогда не изменится на другой тип объекта.
  2. Метод password_field используется для поля ввода пароля. Это приводит к тому, что ввод пользователя отображается в виде точек вместо символов, и поле не будет заполняться, если существующий держатель загружен в форму, например, при редактировании профиля.
  3. Мы будем использовать подтверждение пароля, чтобы лучше защитить владельцев от ошибок при вводе пароля.

Проверьте путь holders/new, и форма должна появиться.

Если бы мы попытались отправить форму в этот момент, мы бы получили ошибку, потому что у нас нет действия создания в контроллере держателей. Создайте это сейчас так же, как мы делали для учетных записей.

def create
  @holder = Holder.new(holder_params)
  if @holder.save
    flash[:notice] = "Your profile was created."
    redirect_to holder_path
  else
    render :new
  end
end
private
def holder_params
  params.require(:holder).permit(:name, :password, :password_confirmation)
end

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

holders_controller.rb:

# inside holders_controller.rb
def show
  @holder = Holder.find params[:id]
end

app/views/holders/show.html.erb:

<%= render 'shared/content_title', title: @holder.name %>

Все, что нужно сделать, это получить объект держателя и отобразить имя держателя. Мы заполним эту страницу позже.

Теперь попробуйте создать новый держатель. Он должен снова выйти из строя, выдавая ошибку, утверждающую unknown attribute: password, что на самом деле верно. У держателя нет атрибута пароля, но есть атрибут password_digest. Было бы ужасно иметь дело с этой сложной строкой, хранящейся в этом столбце, поэтому Rails использует метод под названием has_secure_password (который теперь вы должны включить в верхнюю часть модели держателя), который включает в себя несколько хороших методов для обработки паролей. Одной из таких приятных вещей является создание виртуального атрибута пароля, который позволяет нам делать то, что мы пытаемся сделать.

Нам также нужно добавить проверку, чтобы убедиться, что поля password и password_confirmation в форме идентичны. Добавьте validates :password, confirmation: true к модели Holder.

Попробуйте еще раз создать новый держатель. Еще одна ошибка. Чтобы Rails создал односторонний хэш, ему нужен гем bcrypt-ruby. Включите его в свой Gemfile с помощью gem ‘bcrypt-ruby', ‘~> 3.0.0’. Вам нужно будет снова запустить bundle install и перезапустить сервер, чтобы изменения вступили в силу.

Попробуйте еще раз! Успех! (С надеждой). Теперь приложение должно быть перенаправлено на /holders/[:id] и должно отображать имя держателя.

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

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

Начните с создания кода в файле app/views/layouts/application.html.erb. Добавьте следующее в верхнюю часть тела.

<header>
  <%= render 'layouts/navigation' %>
</header>

Это отобразит часть навигации в папке макетов. Создайте этот файл (_navigation.html.erb) со следующим.

<nav class="navbar navbar-inverse">
  <div class="container">
    <%= link_to "Account Manager", root_path, class: 'navbar-brand' %>
    <ul class="nav navbar-nav navbar-right">
      <li>
        <%= link_to "Register", register_path, class: 'btn' %>
      </li>
    </ul>
  </div>
</nav>

В верхней части каждой страницы появится панель навигации, которая будет содержать название приложения и ссылку для регистрации. Нажмите на ссылку и убедитесь, что она ведет на страницу регистрации. Мы можем использовать register_path, потому что мы указали маршрут as: ‘register’. Щелкните заголовок приложения и убедитесь, что он ведет к корневому пути, который сейчас представляет собой список всех учетных записей.

Вход/выход и сеансы

Владелец может создать свой профиль. Теперь владелец должен иметь возможность войти и выйти из системы. Добавьте собственный маршрут для входа в систему. Мы будем использовать статус входа/выхода в нескольких местах, не все из которых будут иметь дело непосредственно с держателем. Это действие является скорее статусом для приложения и должно быть размещено где-то, что отражает этот факт. Мы создадим действия, связанные с входом/выходом на новом контроллере sessions. Это отклоняется от соглашений Rails, но не требует больших знаний для реализации.

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

get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
get '/logout', to: 'sessions#destroy'

Идея сеанса прекрасно подходит для действий входа/выхода из системы. Нам понадобится контроллер сеанса и представления для отображения и обработки форм.

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

<li>
  <%= link_to "Login", login_path, class: 'btn' %>
</li>

С login_path то же самое, что и с register_path, мы указали это вручную в файле маршрутов. Если вы нажмете кнопку, приложение будет кричать на вас и говорить, что есть uninitialized constant SessionsController. Создайте контроллер сеанса.

def new
end
def create
  holder = Holder.find_by(name: params[:name]);
  if holder && holder.authenticate(params[:password])
    # login
    session[:holder_id] = holder.id
    flash[:notice] = "Welcome, you've logged in."
    redirect_to root_path
  else
    # error on login form
    render :new
  end
end

Новому действию ничего не нужно, так как нет объекта модели, который можно настроить для формы, поддерживаемой моделью. У нас даже нет сеансовой модели. Действие создания должно проверять, соответствует ли предоставленная информация владельцу как имени, так и пароля. Это делается в условном выражении if. holder.authenticate проверяет, что пароль, который они ввели, совпадает с паролем владельца с именем, которое они ввели. Если это так, войдите в систему и сохраните этот статус в сеансе (чтобы им не нужно было входить в систему на каждой отдельной странице). Если есть ошибка, мы на самом деле хотим перенаправить (в отличие от рендеринга представления) на форму входа, чтобы мы могли отображать сообщение об ошибке flash, аналогичное другим формам. Мы должны сделать это, потому что ошибки не привязаны к какому-либо объекту модели, и мы также хотим, чтобы сообщение было преднамеренно расплывчатым.

На этом этапе владелец должен иметь возможность войти в систему.

Реализовать выход из системы. Это довольно просто. Добавьте действие sessions#destroy.

def destroy
  session[:holder_id] = nil
  flash[:notice] = "You have been logged out."
  redirect_to root_path
end

Вот и все. Теперь владелец будет выходить из системы, когда он нажимает кнопку «Выход», и будет перенаправлен на корневой путь. Мы покажем эту кнопку дальше.

Вспомогательные методы для статуса входа и текущего пользователя. Поскольку мы будем принимать несколько решений на основе входа в систему, давайте создадим метод для простой проверки статуса. Это статус для всего приложения, поэтому метод будет помещен в application_controller.rb.

helper_method :current_holder, :logged_in?
def current_holder
  @current_holder ||= Holder.find(session[:holder_id]) if session[:holder_id]
end
def logged_in?
  !!current_holder
end

Будет много действий, связанных с текущим вошедшим в систему держателем, поэтому мы также создадим метод для извлечения этого объекта. logged_in? возвращает логическое значение в зависимости от того, вошел ли держатель в систему, а current_holder возвращает владельца, который вошел в систему.

Условная навигация на основе статуса входа – скройте кнопки входа и регистрации и отобразите кнопку выхода, когда владелец вошел в систему. Измените частичное представление _navigation.html.erb на:

<nav class="navbar navbar-inverse">
  <div class="container">
    <%= link_to "Account Manager", root_path, class: 'navbar-brand' %>
    <ul class="nav navbar-nav navbar-right">
      <% if logged_in? %>
        <li>
          <%= link_to "Logout", logout_path, class: 'btn' %>
        </li>
      <% else %>
        <li>
          <%= link_to "Register", register_path, class: 'btn' %>
        </li>
        <li>
          <%= link_to "Login", login_path, class: 'btn' %>
        </li>
      <% end %>
    </ul>
    <% if logged_in? %>
      <p class="navbar-text navbar-right"><small>Logged in as <%= current_holder.name %>.</small></p>
    <% end %>
  </div>
</nav>

Мы уже использовали методы logged_in? и current_holder в этом партиале. Добавленный здесь код должен говорить сам за себя: при входе в систему отображать, кто вошел в систему, и кнопку выхода из системы; при выходе из системы отображать кнопки регистрации и входа.

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

root to: 'home#index'
get '/', to: 'home#index', as: 'home'

Для этого потребуются два файла:

app/views/home/index.html.erb

<h2>Welcome to Account Manager!</h2>
<p>Please register/login to view and create accounts.</p>

app/controllers/home_controller.rb

class HomeController < ApplicationController
  def index
  end
end

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

Реструктурировать маршрут регистрации/входа. Мы также должны изменить, куда перенаправляется владелец после регистрации и входа в систему.

  • В контроллере сессий измените перенаправление в методе create на redirect_to accounts_path.
  • В контроллере учетных записей измените действие индекса на @accounts = current_holder.accounts. Таким образом, будут отображаться только учетные записи тех, кто вошел в систему.
  • В контроллере владельцев автоматически войдите в систему после успешного создания профиля. Добавьте sessions{:holder_id] = @holder.id в условие if для успешной регистрации в действии create. Перенаправление по-прежнему может быть на действие holder#show.

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

Связать учетные записи с владельцами. Когда учетная запись создается, она не привязывается ни к какому владельцу. Теперь, когда у нас есть держатели, это легко исправить. В действии accounts#create есть следующее:

def create
  @account = Account.new(account_params)
  @account.holders << current_holder
  if @account.save
    flash[:notice] = "Your account was created."
    redirect_to accounts_path
  else
    render :new
  end
end

Помните, что у наших учетных записей может быть более одного держателя (и наоборот), поэтому мы должны использовать @account.holders << current_holder, чтобы добавить первого держателя в список. Потратьте время сейчас, чтобы проверить, все ли работает хорошо. Создайте несколько держателей и создайте учетную запись на каждом из них. Отображаются только аккаунты вошедшего в систему держателя? С надеждой. Опять же, обязательно очистите свою базу данных от любых недействительных данных.

Заворачивать

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

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