Статьи

Управление версиями с помощью PaperTrail

стрелка знак перезагрузить обновить вращение

Представьте себе такую ​​ситуацию: вы открываете страницу администратора вашего сайта, чтобы провести некоторую очистку, находите старые данные, которые никто не просматривал целую вечность, и удаляете их. Удаление успешно, и все в порядке … но через секунду … «НОООО !!! Эти данные содержали ОЧЕНЬ ВАЖНУЮ ИНФОРМАЦИЮ, которая могла бы изменить мир! », Понимаете вы. Но это ушло, и мир остался неизменным (ну, есть шанс на восстановление, если у вас есть резервная копия БД :)).

Можем ли мы предотвратить эту ситуацию в нашем приложении Rails? Да мы можем! paper_trail на помощь!

В этой статье мы поговорим о том, как реализовать страницу «История» и кнопку «отменить» (а также «повторить») с помощью paper_trail .

Исходный код доступен на GitHub .

Рабочая демоверсия доступна по адресу http://undoer.radiant-wind.com/ .

Подготовка демо-проекта

Для этой демонстрации я собираюсь использовать Rails 4.0.4, но такое же решение можно реализовать с помощью Rails 3.

Мы собираемся создать простой блог, который позволит зарегистрированным пользователям добавлять, обновлять и удалять сообщения. Пользователь сможет отменить все действия, связанные с сообщениями (например, отмена непреднамеренного удаления). Мы также предоставим страницу «История», отображающую список действий (и некоторую другую информацию), которые пользователи выполняли при работе с сообщениями.

Хорошо, время начинать. Создайте новое приложение Rails без набора тестов по умолчанию, запустив:

 $ rails new undoer -T 

Вот список драгоценных камней, которые мы собираемся использовать:

Gemfile

 gem 'paper_trail', '~> 3.0.1' gem 'bootstrap-sass', '~> 3.1.1.0' gem 'devise' 

bootstrap-sass включает в себя Twitter Bootstrap, нашего старого друга, который поможет нам стилизовать проект, а devise будет использоваться для настройки некоторой очень простой аутентификации.

paper_trail , созданный Энди Стюартом, является главной звездой этой статьи — он поможет нам создать страницу «История» и кнопку «отменить». Есть некоторые другие гемы, которые обеспечивают версионность, но я считаю paper_trail наиболее удобным. Раньше я пользовался аудированной Коллективной идеей, но она не так полезна и, более того, не обновлялась 8 месяцев. Также он имеет сомнительную совместимость с Rails 4.

Демо-приложение имеет один контроллер (помимо ApplicationController ), PostsController , который будет использоваться для управления нашими сообщениями. Изначально у него есть семь методов по умолчанию: index , create new , create , destroy , edit и update , а также ряд связанных с ними представлений. Я не буду вдаваться в подробности о том, как создавать эти методы и представления, потому что они действительно простые (для краткости вы можете создать их, используя rails g scaffold Posts ).

Демо также содержит две модели: Post и User . Таблица posts имеет следующие столбцы:

  • id
  • title
  • body
  • created_at
  • updated_at

Модель User была сгенерирована Devise и содержит электронную почту вместе с некоторыми другими специальными полями. Опять же, я не буду вдаваться в подробности настройки Devise, потому что это не связано с темой статьи. Для начала вы можете использовать следующее руководство: https://github.com/plataformatec/devise#getting-started . Кроме того, не стесняйтесь любой подход аутентификации.

Файл маршрутов выглядит так:

routes.rb

 devise_for :users root to: 'posts#index' resources :posts 

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

Запустите эти команды:

 $ bundle exec rails generate paper_trail:install $ bundle exec rake db:migrate 

Это создаст и применит необходимую миграцию. Теперь добавьте следующую строку в вашу модель Post :

модели / post.rb

 class Post < ActiveRecord::Base has_paper_trail end 

И это все! Теперь все изменения в таблице posts будут проверяться автоматически. Как это круто?

Вы также можете указать, какие изменения не должны отслеживаться. Например, если в posts был столбец view_count и мы не хотели отслеживать внесенные в него изменения, изменение выглядит так:

модели / post.rb

 has_paper_trail ignore: [:view_count] 

Некоторые поля могут быть пропущены полностью, то есть они не будут ни отслеживаться, ни включаться в сериализованную версию объекта):

модели / post.rb

 has_paper_trail skip: [:view_count] 

Также можно указать события:

модели / post.rb

 has_paper_trail on: [:update, :create] 

или укажите условия для отслеживания события:

модели / post.rb

 has_paper_trail if: Proc.new { |t| t.title.length > 10 }, unless: Proc.new { |t| t.body.blank? } 

Простой, но мощный.

Отображение журнала

Теперь вы, вероятно, хотите отобразить проверенные версии, а? Это просто, просто добавьте следующий маршрут:

routes.rb

 [...] get '/posts/history', to: 'posts#history', as: :posts_history resources :posts 

на страницу истории для сообщений. Если в вашем приложении проверено много моделей, рекомендуется создать отдельный VersionsController и поместить в него все методы, связанные с версиями. Однако в нашем случае проверяется только модель Post , поэтому давайте остановимся на одном контроллере.

Добавьте новый метод в контроллер:

Контроллеры / posts_controller.rb

 def history @versions = PaperTrail::Version.order('created_at DESC') end 

Обратите внимание, что мы должны использовать PaperTrail::Version , а не только Version . Эта строка кода извлекает все записанные события из таблицы versions которую мы создали ранее, и сортирует их по дате создания. В реальном приложении рекомендуется kaminari эти события на страницы с помощью will_paginate или kaminari gem.

Визуализация событий:

сообщений / history.html.erb

 <div class="container"> <h1>History</h1> <ul> <% @versions.each do |version| %> <li> <%= l(version.created_at, format: "%-d.%m.%Y %H:%M:%S %Z") %><br/> Event ID: <%= version.id %><br/> <b>Target:</b> <%= version.item_type %> <small>(id: <%= version.item_id %>)</small>; <b>action</b> <%= version.event %>;<br/> <div> More info: <pre><%= version.object %></pre> </div> </li> <% end %> </ul> </div> 

Вот данные, которые отображаются:

  • version.created_at — когда произошло это событие.
  • version.id — идентификатор этого события.
  • version.item_type — Название модели для события. В нашем случае это Post .
  • version.item_id — идентификатор ресурса (сообщения), который был изменен.
  • version.event — действие, примененное к ресурсу (создание, обновление, уничтожение).
  • version.object — Полный дамп ресурса, который был изменен.

Все идет нормально. Тем не менее, есть некоторые вещи, которые можно улучшить. Например, какие поля были изменены (особенно для действия обновления)? Ну, это очень легко реализовать.

Создайте и примените следующую миграцию:

 $ rails g migration add_object_changes_to_versions object_changes:text $ rake db:migrate 

Это также можно сделать при настройке paper_trail . Вам просто нужно предоставить соответствующую опцию для генератора:

 $ rails generate paper_trail:install --with-changes 

Никаких дальнейших действий не требуется, paper_trail автоматически paper_trail версии.

Теперь мы можем добавить новый блок div в наше представление:

сообщений / history.html.erb

 [...] <div> Changeset: <pre><%= version.changeset %></pre> </div> [...] набор [...] <div> Changeset: <pre><%= version.changeset %></pre> </div> [...] 

Это отобразит значения атрибута до и после события (если атрибут остался неизменным, он не будет отображаться).

Отслеживание пользовательской информации

Хорошо, теперь мы знаем, когда мы удалили наш драгоценный пост в блоге. Но мы не знаем, кто ! Самое время исправить эту проблему.

Давайте отследим IP-адрес пользователя, ответственного за действие. Конечно, IP-адрес может быть подделан, но главное здесь — объяснить, как хранить метаданные вместе с данными события. Перейдите и создайте новую миграцию:

 $ rails g migration add_ip_to_versions ip:string $ rake db:migrate 

paper_trail по умолчанию ничего не будет хранить в столбце ip , поэтому нам нужно немного помочь. Добавьте этот метод в ApplicationController :

Контроллеры / application_controller.rb

 def info_for_paper_trail # Save additional info { ip: request.remote_ip } end 

paper_trail будет использовать этот метод для получения дополнительной информации и сохранения ее в качестве метаданных. Если вы используете Rails 3 или protected_attributes с Rails 4, вам также необходимо создать инициализатор:

Инициализаторы / paper_trail.rb

 module PaperTrail class Version < ActiveRecord::Base attr_accessible :ip end end 

Метаданные также могут быть предоставлены в модели следующим образом (при условии, что у нас есть столбец timestamp ):

модели / post.rb

 has_paper_trail meta: { timestamp: Time.now } 

Последнее, что нужно сделать, это добавить строку в представление:

сообщений / history.html.erb

 [...] <b>Remote address:</b> <%= version.ip %><br/> [...] 

Хранение IP — это хорошо, но помните, что у нас настроена базовая система аутентификации — не могли бы мы также сохранить пользователя, ответственного за действие? Это было бы очень удобно! Конечно, мы можем сделать это также.

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

Контроллеры / application_controller.rb

 [...] def user_for_paper_trail # Save the user responsible for the action user_signed_in? ? current_user.id : 'Guest' end [...] 

Пользователь user_signed_in? Метод предоставляется Devise — в основном, он сообщает, вошел ли пользователь в систему или нет. current_user определяется Devise и возвращает пользователя, вошедшего в данный момент в качестве ресурса (если пользователь не вошел в систему, он возвращает nil .) Таким образом, мы можем легко получить id текущего пользователя. Пока что только пользователи, прошедшие проверку, могут управлять публикациями в нашем блоге, но это может измениться. В случае отсутствия регистрации мы указываем «Гость».

Последнее, что нужно сделать, это отобразить эту новую информацию:

сообщений / history.html.erb

 [...] <b>User:</b> <% if version.whodunnit && version.whodunnit != 'Guest' %> <% user = User.find_by_id(version.whodunnit) %> <% if user %> <%= user.email %> (last seen at <%= l(user.last_sign_in_at, format: "%-d.%m.%Y %H:%M:%S") %>) <% end %> <% else %> Guest <% end %> [...] 

На нашей странице «История» теперь представлена ​​действительно полезная информация. Мы можем отслеживать, когда произошло событие, что было изменено, как выглядел ресурс до изменения и кто несет ответственность за это изменение. Потрясающие!

Отмена действия

Давайте перейдем ко второй части, позволяющей нашим пользователям отменять свои действия.

Для этого создайте новый метод, который отменит запрошенное действие:

Контроллеры / posts_controller.rb

 def undo @post_version = PaperTrail::Version.find_by_id(params[:id]) begin if @post_version.reify @post_version.reify.save else # For undoing the create action @post_version.item.destroy end flash[:success] = "Undid that!" rescue flash[:alert] = "Failed undoing the action..." ensure redirect_to root_path end end 

Выше найдите версию по идентификатору (мы сгенерируем соответствующую ссылку позже). Затем проверьте, есть ли предыдущие версии, доступные для ресурса, используя метод reify . Этот метод вернет nil если ресурс был только что создан в текущей версии (очевидно, если ресурс был только что создан, у него нет предыдущих версий). Либо откат к предыдущей версии с использованием @post_version.reify.save либо @post_version.reify.save нового созданный ресурс с использованием @post_version.item.destroy ( @post_version.item возвращает фактический ресурс). Просто, не правда ли?

Конечно, нам нужно добавить новый маршрут:

routes.rb

 [...] post '/posts/:id/undo', to: 'posts#undo', as: :undo resources :posts [...] 

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

макеты / application.html.erb

 <div class="container"> <% flash.each do |key, value| %> <div class="alert alert-<%= key %>"> <button type="button" class="close" data-dismiss="alert">&times;</button> <%= value.html_safe %> </div> <% end %> </div> 

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

Создайте приватный метод в PostsController который генерирует ссылку отмены:

Контроллеры / posts_controller.rb

 [...] private def make_undo_link view_context.link_to 'Undo that plz!', undo_path(@post.versions.last), method: :post end 

Мы не можем использовать link_to внутри контроллера, поэтому ссылка на view_context указывает на фактическое представление. @post.versions извлекает все версии для ресурса Post а @post.versions.last самую последнюю. Этот метод можно использовать так:

Контроллеры / posts_controller.rb

 [...] def update @post = Post.find_by_id(params[:id]) if @post.update_attributes(post_params) flash[:success] = "Post was updated! #{make_undo_link}" redirect_to post_path(@post) else render 'edit' end end [...] 

Обязательно добавьте его в методы create и destroy .

Попробуйте и попробуйте в демонстрационном приложении. Обратите внимание, что отмена действия также отслеживается paper_trail .

Повторить действие

Хорошо, я отменил действие … но теперь я хочу повторить его. Взрыва. Здесь мы должны ввести ссылку возврата, которая отменяет отмену. Есть только несколько модификаций, необходимых.

Создайте еще один приватный метод:

Контроллеры / posts_controller.rb

 [...] private def make_redo_link params[:redo] == "true" ? link = "Undo that plz!" : link = "Redo that plz!" view_context.link_to link, undo_path(@post_version.next, redo: !params[:redo]), method: :post end [...] 

Этот метод очень похож на make_undo_link . Основным отличием является params[:redo] который является либо true либо false . Исходя из этого параметра, измените текст ссылки — URL фактически остается неизменным. Это потому, что повторение в основном означает возврат к предыдущей версии, которая абсолютно совпадает с действием отмены.

Измените флэш-сообщение в методе undo :

Контроллеры / posts_controller.rb

 [...] def undo [...] flash[:success] = "Undid that! #{make_redo_link}" [...] 

Это все! Пользователи могут отменять и повторять свои действия столько раз, сколько захотят, каждый раз, когда они записываются paper_trail .

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

 PaperTrail::Version.delete_all ["created_at < ?", 1.week.ago] 

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

 PaperTrail.config.version_limit = 3 

Вывод

Это подводит нас к концу статьи. Имейте в виду, что с paper_trail вы можете сделать гораздо больше, чем мы обсуждали. Например, вы можете выбрать версии в определенный момент времени ( @post.version_at(1.day.ago) ) или @post.version_at(1.day.ago) между версиями ( @post.previous_version , @post.next_version ) или работать с ассоциациями моделей. Вы даже можете создать систему, которая использует две произвольные версии ресурса (аналогично системе сравнения, которая реализована на вики-сайтах).

Я надеюсь, что эта статья была полезна и интересна для вас. Если нет, я могу переделать это (стон)…