Статьи

Ленты активности с Rails

Снимок экрана 2015-02-22 08.05.08

Ленты новостей повсюду. Например, на главной странице Github перечислены все действия в вашей сети социального кодирования: один из ваших друзей создал или клонировал репо, был открыт новый выпуск, ваш репо был разветвлен или помечен, и т. Д.

GitHub

Twitter отображает последние твиты, ретвиты и ответы.

щебет

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

В этой статье я собираюсь поговорить о public_activity , драгоценном камне, созданном Piotrek Okoński, чтобы легко отслеживать активность моделей (и даже больше). Я покажу вам, как легко создать ленту действий и использовать различные функции public_activity для дальнейшего расширения приложения.

Прежде чем продолжить, я хотел сделать небольшую заметку. Несколько месяцев назад я написал статью « Управление версиями с помощью PaperTrail», в которой рассказывалось о paper_trail , драгоценном камне, который использовался для контроля версий моделей приложения. В некотором смысле paper_trail похож на public_activity и может быть использован для реализации решения, аналогичного представленному в этой статье. Однако paper_trail предназначен для создания систем управления версиями, тогда как public_activity был создан специально для реализации каналов активности.

Рабочая демоверсия доступна по адресу sitepoint-public-activity.herokuapp.com .

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

Подготовка приложения

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

Время для земляных работ. Давайте назовем наше приложение Storyteller:

 $ rails new Storyteller -T 

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

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

Gemfile

 [...] gem 'bootstrap-sass', '~> 3.3.1' gem 'autoprefixer-rails' gem 'public_activity' gem 'omniauth-facebook' [...] 

и беги

 $ bundle install 

bootstrap-sass autoprefixer-rails и autoprefixer-rails являются обязательными — первый используется для стилизации, а второй автоматически добавляет префиксы поставщиков браузера в правила CSS. public_activity — звезда, так как она поможет нам настроить канал активности. omniauth-facebook будет использоваться для настройки аутентификации.

Если вы следуете, подключите стили Bootstrap:

application.scss

 @import "bootstrap-sprockets"; @import "bootstrap"; @import 'bootstrap/theme'; 

Теперь нам нужно подготовить пару моделей. Первый будет называться Story и будет содержать следующие атрибуты (я пропускаю id по умолчанию, created_at и updated_at ):

  • title ( string ) — название рассказа
  • body ( text ) — тело истории
  • user_id ( integer ) — внешний ключ для ссылки на автора истории

Второй будет называться User и будет иметь следующие атрибуты:

  • name ( string ) — имя (возможно, с фамилией) пользователя
  • uid ( string ) — уникальный идентификатор пользователя, предоставленный социальной сетью
  • avatar_url ( string ) — URL аватара пользователя

Создайте и примените необходимые миграции:

 $ rails g model User name:string uid:string:index avatar_url:string $ rails g model Story title:string body:text user:references $ rake db:migrate 

Настройте файлы модели следующим образом:

user.rb

 class User < ActiveRecord::Base has_many :stories end 

story.rb

 class Story < ActiveRecord::Base belongs_to :user validates :title, presence: true validates :body, presence: true end 

Настройте маршруты:

 [...] resources :stories delete '/logout', to: 'sessions#destroy', as: :logout get '/auth/:provider/callback', to: 'sessions#create' root to: 'stories#index' [...] 

/auth/:provider/callback — это маршрут обратного вызова, используемый Facebook как часть процесса входа в OAuth2. Часть :provider означает, что вы можете использовать любую другую стратегию аутентификации Omniauth (или несколько стратегий одновременно).

Сосредоточиться на макете сейчас:

просмотров / макеты / application.html.erb

 [...] <div class="navbar navbar-inverse"> <div class="container"> <div class="navbar-header"> <%= link_to 'Storyteller', root_path, class: 'navbar-brand' %> </div> <ul class="nav navbar-nav pull-right"> <% if current_user %> <li><span><%= image_tag current_user.avatar_url, alt: current_user.name %></span></li> <li><%= link_to 'Log Out', logout_path, method: :delete %></li> <% else %> <li><%= link_to 'Log In', '/auth/facebook' %></li> <% end %> </ul> </div> </div> <div class="container"> <div class="page-header"> <h1><%= yield :page_header %></h1> </div> <% flash.each do |key, value| %> <div class="alert alert-<%= key %>"> <%= value %> </div> <% end %> <%= yield %> </div> [...] 

Здесь нет ничего особенного, кроме части кода yield :page_header которая использует вспомогательный метод page_header . Создайте это сейчас:

application_helper.rb

 module ApplicationHelper def page_header(header) content_for(:page_header) {header.to_s} end end 

Отлично, следующий шаг — создание контроллера, чтобы собрать все это вместе:

stories_controller.rb

 class StoriesController < ApplicationController before_action :find_story, only: [:destroy, :show, :edit, :update] def index @stories = Story.order('created_at DESC') end def new @story = Story.new end def create @story = Story.new(story_params) if @story.save flash[:success] = 'Your story was added!' redirect_to root_path else render 'new' end end def edit end def update if @story.update_attributes(story_params) flash[:success] = 'The story was edited!' redirect_to root_path else render 'edit' end end def destroy if @story.destroy flash[:success] = 'The story was deleted!' else flash[:error] = 'Cannot delete this story...' end redirect_to root_path end def show end private def story_params params.require(:story).permit(:title, :body) end def find_story @story = Story.find(params[:id]) end end 

Это очень простой контроллер, но немного интереснее.

Теперь мнения:

просмотров / рассказы / index.html.erb

 <% page_header "Our cool stories" %> <p><%= link_to 'Tell one!', new_story_path, class: 'btn btn-primary btn-large' %></p> <% @stories.each do |story| %> <div class="well well-lg"> <h2><%= link_to story.title, story_path(story) %></h2> <p><%= truncate(story.body, length: 350) %></p> <div class="btn-group"> <%= link_to 'Edit', edit_story_path(story), class: 'btn btn-info' %> <%= link_to 'Delete', story_path(story), data: {confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-danger' %> </div> </div> <% end %> 

Как видите, мы вызываем вспомогательный метод page_header который был определен некоторое время назад. Вот что отображается для каждой истории:

просмотров / рассказы / show.html.erb

 <% page_header @story.title %> <p><%= @story.title %></p> <div class="btn-group"> <%= link_to 'Edit', edit_story_path(@story), class: 'btn btn-info' %> <%= link_to 'Delete', story_path(@story), data: {confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-danger' %> </div> 

new и edit виды еще проще:

просмотров / рассказы / new.html.erb

 <% page_header "New cool story" %> <%= render 'form' %> 

просмотров / рассказы / edit.html.erb

 <% page_header "Edit cool story" %> <%= render 'form' %> 

Частичное для формы для создания истории:

просмотров / рассказы / _form.html.erb

 <%= form_for @story do |f| %> <%= render 'shared/errors', object: @story %> <div class="form-group"> <%= f.label :title %> <%= f.text_field :title, class: 'form-control', required: true %> </div> <div class="form-group"> <%= f.label :body %> <%= f.text_area :body, class: 'form-control', required: true, cols: 3 %> </div> <%= f.submit 'Post', class: 'btn btn-primary' %> <% end %> 

shared/_errors приведена здесь, поэтому мы также должны создать ее:

просмотров / общий / _errors.html.erb

 <% if object.errors.any? %> <div class="panel panel-danger"> <div class="panel-heading"> <h3 class="panel-title">The following errors were found while submitting the form:</h3> </div> <div class="panel-body"> <ul> <% object.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> </div> <% end %> 

И некоторые основные стили:

application.scss

 [...] .well { h2 { margin-top: 0; } } [...] 

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

Аутентификация через Facebook

Создайте новый файл omniauth.rb в каталоге config / initializers со следующим содержимым:

конфиг / Инициализаторы / omniauth.rb

 Rails.application.config.middleware.use OmniAuth::Builder do provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'], scope: 'public_profile' end 

Чтобы получить ключ и секрет Facebook, посетите страницу developers.facebook.com и создайте новое приложение (с типом «Веб-сайт»). На странице Dashboard недавно созданного приложения найдите «Идентификатор приложения» и «Секрет приложения» (для его отображения потребуется указать пароль) — это то, что вам нужно. Примечание: пара ключей не должна быть доступна публично — я использую переменные среды для ее хранения.

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

  • URL сайта (если вы тестируете приложение на локальном компьютере, введите «http: // localhost: 3000»)
  • Домены приложений (если вы находитесь на локальном компьютере, оставьте это поле пустым)
  • Почта для связи

Нажмите «Сохранить изменения».

Наконец, перейдите к «Статус и обзор» и переключите «Хотите ли вы сделать это приложение и все его живые функции доступными для широкой публики?» На «Да». Ваше приложение теперь доступно, и пользователи могут войти, используя его.

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

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

sessions_controller.rb

 class SessionsController < ApplicationController def create user = User.from_omniauth(request.env['omniauth.auth']) session[:user_id] = user.id flash[:success] = "Welcome, #{user.name}" redirect_to root_url end def destroy session[:user_id] = nil flash[:success] = "Goodbye!" redirect_to root_url end end 

request.env['omniauth.auth'] содержит всю информацию о пользователе. Необходимо User.from_omniauth метод класса User.from_omniauth :

модели / user.rb

 class << self def from_omniauth(auth) user = User.find_or_initialize_by(uid: auth['uid']) user.name = auth['info']['name'] user.avatar_url = auth['info']['image'] user.save! user end end 

Мы храним необходимую информацию и в результате возвращаем объект user . Метод find_or_initialize_by создаст нового пользователя или обновит существующего, если uid уже присутствует в базе данных. Это сделано для предотвращения создания одного и того же пользователя несколько раз.

И, наконец, метод current_user , который возвращает текущего зарегистрированного пользователя или nil :

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

 [...] private def current_user @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] end helper_method :current_user [...] 

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

Когда вы закончите, загрузите сервер. Попробуйте пройти проверку подлинности и добавьте пару историй, чтобы убедиться, что все работает. Мы готовы перейти к интересной части.

Интеграция public_activity

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

Сейчас давайте сделаем базовую настройку. Запустите эти команды:

 $ rails g public_activity:migration $ rake db:migrate 

Это сгенерирует и применит public_activity . Создана новая таблица с названием activities .

Чтобы включить отслеживание для модели Story :

модели / story.rb

 [...] include PublicActivity::Model tracked [...] 

Довольно просто, не правда ли? Теперь, когда вы выполняете такие действия, как save , update_attributes , destroy и другие, будет public_activity обратный вызов public_activity для записи этого события.

Этот гем также поддерживает адаптеры MongoMapper и Mongoid — обратитесь к разделу Настройка базы данных в документации, чтобы узнать больше.

Отображение канала активности

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

Настроить контроллер:

stories_controller.rb

 [...] before_action :load_activities, only: [:index, :show, :new, :edit] private def load_activities @activities = PublicActivity::Activity.order('created_at DESC').limit(20) end [...] 

Как видите, модель Activity находится внутри пространства имен PublicActivity чтобы предотвратить конфликты имен. Мы заказываем мероприятия по дате создания (самые новые — первые) и принимаем первые двадцать из них. Узнайте больше здесь .

Наш макет должен быть немного изменен, чтобы лента активности размещалась справа от сайта:

просмотров / макеты / application.html.erb

 [...] <div class="col-sm-9"> <%= yield %> </div> <%= render 'shared/activities' %> [...] 

просмотров / общий / _activities.html.erb

 <div class="col-sm-3"> <ul class="list-group"> <%= @activities.inspect %> </ul> </div> 

Для тех, кто не знает, Bootstrap использует сетку с 12 столбцами, поэтому, указав col-sm-9 мы используем 9 столбцов (75% доступного пространства) для основного содержимого. col-sm-3 , в свою очередь, оставляет 3 столбца для канала активности. sm здесь означает, что столбцы будут отображаться одна под другой (горизонтальная сетка) на небольших дисплеях. Более подробная информация доступна здесь .

Перезагрузите страницу, чтобы проверить, что @activities массив @activities и как он отображается. Для рендеринга массива действий существует специальный вспомогательный метод render_activities :

просмотров / общий / _activities.html.erb

 <div class="col-sm-3"> <%= render_activities @activities %> </div> 

public_activity ожидает, что внутри представлений есть папка public_activity, которая, в свою очередь, имеет папку истории (или любую другую папку с единичным именем модели, связанной с конкретной деятельностью). Внутри каталога истории должны быть следующие компоненты : _create.html.erb , _update.html.erb , _destroy.html.erb . Каждый фрагмент, как вы, вероятно, догадались, отображается для соответствующего действия. Внутри этих партиалов есть локальная переменная activity (с псевдонимом a ).

Идем дальше и создаем эти файлы:

просмотров / public_activity / история / _create.html.erb

 <li class="list-group-item"> <%= a.trackable.title %> was added. </li> 

просмотров / public_activity / история / _update.html.erb

 <li class="list-group-item"> <%= a.trackable.title %> was edited. </li> 

просмотров / public_activity / история / _destroyed.html.erb

 <li class="list-group-item"> <%= a.trackable.title %> was deleted. </li> 

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

Однако есть проблема. Если вы создадите, а затем удалите историю, вы увидите ошибку undefined method 'title' for nil:NilClass . Это потому, что мы пытаемся получить название записи, которая была удалена. Это достаточно легко исправить:

просмотров / public_activity / история / _create.html.erb

 <li class="list-group-item"> <% if a.trackable %> <%= a.trackable.title %> was created. <% else %> An article that is currently deleted was added. <% end %> </li> 

просмотров / public_activity / история / _update.html.erb

 <li class="list-group-item"> <% if a.trackable %> <%= a.trackable.title %> was edited. <% else %> An article that is currently deleted was edited. <% end %> </li> 

просмотров / public_activity / история / _destroyed.html.erb

 <li class="list-group-item"> An article was deleted. </li> 

Довольно мило, но не очень информативно. Когда происходило действие? Можем ли мы перейти непосредственно к статье, которая была изменена? Кто это изменил? Что ж, первые две проблемы можно легко исправить:

просмотров / public_activity / история / _create.html.erb

 <li class="list-group-item"> <span class="glyphicon glyphicon-plus"></span> <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/> <% if a.trackable %> <%= link_to a.trackable.title, story_path(a.trackable) %> was added. <% else %> An article that is currently deleted was added. <% end %> </li> 

просмотров / public_activity / история / _update.html.erb

 <li class="list-group-item"> <span class="glyphicon glyphicon-edit"></span> <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/> <% if a.trackable %> <%= link_to a.trackable.title, story_path(a.trackable) %> was edited. <% else %> An article that is currently deleted was edited. <% end %> </li> 

просмотров / public_activity / история / _destroyed.html.erb

 <li class="list-group-item"> <span class="glyphicon glyphicon-remove"></span> <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/> An article was deleted. </li> 

Я также добавил несколько Bootstrap Glyphicons, чтобы все выглядело немного красивее.

Отображение информации о пользователе, ответственном за изменение, требует немного больше работы.

Хранение информации о пользователе

В таблице activities есть специальное поле с именем owner , предназначенное для хранения информации о пользователе, ответственном за действие. Проблема, однако, заключается в том, что метод current_user недоступен внутри модели, поэтому мы должны использовать довольно хакерское решение .

Настроить контроллер:

application_controller.rb

 [...] include PublicActivity::StoreController def current_user @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] end helper_method :current_user hide_action :current_user [...] 

Обратите внимание, что я удалил ключевое слово private потому что иначе мы не сможем вызвать current_user из модели. Добавление hide_action :current_user гарантирует, что этот метод не считается действием контроллера.

Теперь модель:

модели / story.rb

 [...] tracked owner: Proc.new { |controller, model| controller.current_user ? controller.current_user : nil } [...] 

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

После этого войдите в систему, добавьте / измените некоторые истории и проверьте таблицу activities . Поле owner должно быть заполнено идентификатором пользователя.

Последний шаг — модификация частичек:

просмотров / public_activity / история / _create.html.erb

 <li class="list-group-item"> <span class="glyphicon glyphicon-plus"></span> <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/> <strong><%= activity.owner ? activity.owner.name : 'Guest' %></strong> <% if a.trackable %> added the story <%= link_to a.trackable.title, story_path(a.trackable) %>. <% else %> added the story that is currently deleted. <% end %> </li> 

просмотров / public_activity / история / _update.html.erb

 <li class="list-group-item"> <span class="glyphicon glyphicon-edit"></span> <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/> <strong><%= activity.owner ? activity.owner.name : 'Guest' %></strong> <% if a.trackable %> edited the story <%= link_to a.trackable.title, story_path(a.trackable) %>. <% else %> edited the story that is currently deleted. <% end %> </li> 

просмотров / public_activity / история / _destroyed.html.erb

 <li class="list-group-item"> <span class="glyphicon glyphicon-remove"></span> <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/> <strong><%= activity.owner ? activity.owner.name : 'Guest' %></strong> deleted a story. </li> 

Теперь для вновь выполненных действий вы увидите свое имя; для предыдущих должна быть указана строка «Гость».

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

Использование откатов I18n для рефакторинга частичных действий

Я хочу полностью избавиться от этих частичных действий и работать только с файлом shared / _activities.html.erb . Вы должны знать, однако, что есть некоторые другие возможные решения .

Основная структура частичного будет следующим:

просмотров / общий / _activities.html.erb

 <div class="col-sm-3"> <ul class="list-group"> <% @activities.each do |activity| %> <li class="list-group-item"> <!-- render activities here --> </li> <% end %> </ul> </div> 

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

Проблема с иконкой может быть легко решена с помощью регулярных выражений и некоторой магии Sass:

просмотров / общий / _activities.html.erb

 <div class="col-sm-3"> <ul class="list-group"> <% @activities.each do |activity| %> <li class="list-group-item"> <span class="glyphicon glyphicon-<%= activity.key.match(/\.(.*)/)[1] %>"></span> </li> <% end %> </ul> </div> 

key поля содержат строку в виде . так, например, в нашем случае это может быть story.create , story.update или story.destroy . Конечно, Bootstrap не применяет стили к таким классам, как glyphicon-create но его можно легко изменить:

application.scss

 .glyphicon-update { @extend .glyphicon-edit; } .glyphicon-create { @extend .glyphicon-plus; } .glyphicon-destroy { @extend .glyphicon-remove; } 

Мы используем директиву @extend Sass для применения стилей к нашим новым классам.

Проблема с текстом может быть решена с помощью запасных вариантов. Как мы уже видели, public_activity по умолчанию будет искать партиалы внутри public_activity / каталог. Однако, если мы предоставим опцию display: :i18n , вместо этого будут использоваться переводы I18n .

Структура этих переводов выглядит так:

 activity: model_name: create: '...' destroy: '...' update: '...' other_action: '...' 

Таким образом, мы также решаем возможную проблему интернационализации, которая может возникнуть в будущем.

Частичное содержит следующий код:

просмотров / общий / _activities.html.erb

 <div class="col-sm-3"> <ul class="list-group"> <% @activities.each do |activity| %> <li class="list-group-item"> <span class="glyphicon glyphicon-<%= activity.key.match(/\.(.*)/)[1] %>"></span> <strong><%= activity.owner ? activity.owner.name : 'Guest' %></strong> <%= render_activity activity, display: :i18n %> <% if activity.trackable %> "<%= link_to activity.trackable.title, story_path(activity.trackable) %>" <% else %> with unknown title. <% end %> </li> <% end %> </ul> </div> 

Здесь render_activity вспомогательный метод render_activity — его псевдоним render_activities мы видели ранее.

Теперь файл переводов:

конфиг / локали / en.yml

 en: activity: story: create: 'has told his story' destroy: 'has removed the story' update: 'has edited the story' 

Ранее созданная папка public_activity может быть полностью удалена.

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

Создание пользовательских действий

До сих пор мы работали только с базовыми операциями CRUD, которые приводили к автоматическому сохранению действий. Но что, если мы хотим отслеживать некоторые пользовательские события? Или, если есть необходимость активировать действие, не касаясь модели?

Не волнуйтесь, это можно сделать довольно легко. Предположим, мы хотим добавить кнопку «Мне нравится» и подсчитывать лайки для каждого сообщения. Кроме того, особая активность должна быть записана тоже.

Прежде всего, нам нужно добавить новый столбец в таблицу stories :

 $ rails g migration add_likes_to_stories likes:integer $ rake db:migrate 

Теперь новый маршрут:

конфиг / routes.rb

 [...] resources :stories do member do post :like end end [...] 

Добавьте кнопку «Мне нравится» в представление:

просмотров / рассказы / show.html.erb

 <% page_header @story.title %> <p> <span class="label label-default"><%= pluralize(@story.likes, 'like') %></span> <%= link_to content_tag(:span, '', class: 'glyphicon glyphicon-thumbs-up') + ' Like it', like_story_path(@story), class: 'btn btn-default btn-sm', method: :post %> </p> [...] 

И действие контроллера:

stories_controller.rb

 before_action :find_story, only: [:destroy, :show, :edit, :update, :like] [...] def like @story.increment!(:likes) @story.create_activity :like flash[:success] = 'Thanks for sharing your opinion!' redirect_to story_path(@story) end [...] 

@story.increment!(:likes) просто добавляет 1 к @story.increment!(:likes) и сохраняет результат в базе данных. @story.create_activity :like самом деле создает новое действие, предоставляя ключ like (мы говорили о ключах ранее при рефакторинге партиалов). Это потребует от нас изменения файла переводов:

конфиг / локали / en.yml:

 en: activity: story: like: 'has liked the story' [...] 

Если вместо этого вы имеете дело с частичными версиями, то вам придется создать частичное представление views / public_activity / story / _like.html.erb .

Метод create_activity вызывается для запуска пользовательского действия — он не требует изменения модели.

Однако мы еще не закончили. Есть одна проблема, которая позволит мне показать еще одну особенность public_activityотключение отслеживания модели . Видите ли, @story.increment!(:likes) запускает обновление, в результате которого public_activity записывает событие обновления. Итак, @story.create_activity :like приведет к записи двух действий для одного действия. Это явно не то, что мы хотим. Первая операция должна быть сделана без какого-либо отслеживания вообще.

public_activity позволяет отключить отслеживание глобально или для конкретной модели. Для глобального отключения используйте

 PublicActivity.enabled = false 

Чтобы отключить отслеживание на уровне модели, используйте

 Story.public_activity_off 

Мы собираемся использовать последнее решение, так как первое явно излишне:

stories_controller.rb

 def like Story.public_activity_off @story.increment!(:likes) Story.public_activity_on @story.create_activity :like flash[:success] = 'Thanks for sharing your opinion!' redirect_to story_path(@story) end 

Это может быть упрощено далее:

stories_controller.rb

 def like without_tracking do @story.increment!(:likes) end @story.create_activity :like flash[:success] = 'Thanks for sharing your opinion!' redirect_to story_path(@story) end private def without_tracking Story.public_activity_off yield if block_given? Story.public_activity_on end 

Теперь операция приращения не вызовет update . Большой!

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

Предположим, мы хотим сохранить некоторую дополнительную информацию об активности. Как мы могли это сделать?

public_activity представляет два способа достижения желаемого результата. Прежде всего, в таблице activities есть поле сериализованных parameters к которому мы можем получить доступ сразу:

 @story.create_activity :like, parameters: {why: 'because'} 

Позже мы можем получить доступ к этой информации, например:

 activity.parameters['why'] 

Это не всегда удобно, однако. Если вам нужно сохранить некоторые данные для каждого действия, этот подход требует вызова create_activity для каждого действия.

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

Настройка очень проста. Создайте новую миграцию:

xxx_add_title_to_activities.rb

 class AddTitleToActivities < ActiveRecord::Migration def change change_table :activities do |t| t.string :title end end end 

и применить его:

 $ rake db:migrate 

А теперь настройте модель так:

модели / story.rb

 [...] tracked owner: Proc.new { |controller, model| controller.current_user ? controller.current_user : nil }, title: Proc.new { |controller, model| model.title } [...] 

Напомню, эта model хранит объект, который был изменен.

И теперь мы можем отобразить этот заголовок в случае, если модель уничтожена:

общий / _activities.html.erb

 <div class="col-sm-3"> <ul class="list-group"> <% @activities.each do |activity| %> <li class="list-group-item"> <span class="glyphicon glyphicon-<%= activity.key.match(/\.(.*)/)[1] %>"></span> <small class="text-muted"><%= activity.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/> <strong><%= activity.owner ? activity.owner.name : 'Guest' %></strong> <%= render_activity(activity, display: :i18n) %> <% if activity.trackable %> "<%= link_to activity.trackable.title, story_path(activity.trackable) %>" <% elsif activity.title %> <span class="text-muted">"<%= activity.title %>"</span> <% else %> with unknown title. <% end %> </li> <% end %> </ul> </div> 

Вывод

Я показал вам почти все функции, предоставляемые public_activity — надеюсь, вы будете использовать его в одном из своих будущих проектов!

Вы когда-нибудь использовали подобный канал активности? Какие инструменты вы использовали? Поделитесь своим опытом! Как всегда, ваши отзывы или статьи приветствуются.

Счастливого взлома!