Skip to content

Instantly share code, notes, and snippets.

@jules2689
Last active January 31, 2019 10:17
Show Gist options
  • Save jules2689/cb7caafb46094615b415 to your computer and use it in GitHub Desktop.
Save jules2689/cb7caafb46094615b415 to your computer and use it in GitHub Desktop.
Tracking users' actions throughout an application in an "activity feed"

What this does

This is the initial code. Improvements are likely to be had.

  • This creates an activity feed that can track updates, destroys, creates in a MySQL database.
  • This is inspired by part of the "public_activity" gem.
  • For Updates, what changed will be automatically stored as a hash.
  • Bootstrap's usage is assumed, as is Devise's "current_user". ActiveRecord must be used as well. These can easily be changed however.

How to use it

  • The migration should be placed as a normal migration in the migrations folder and the activity.rb in the models folder. This will be the base of the work
  • base_model.rb should also be placed in the model folder and anything you would like to track should extend this model. This will default the tracking of updates, creates, and destroys.
  • 2 ERB snippets are included to help with rendering nice views. The see_more.js.coffee should be included to make the "see more" buttons on updates work.
  • You can track custom events by including an activity call. Activity.register_activity(User.current_user, self, "changed their password", User.current_ip_address). The second arugment is what you are changing, the third argument is an action (or what happened).
  • Include a partial render call in a view you want to see the activity list <%= render "activities/activities", activities: current_user.activities %>. Note that the ERB templates are placed within an activities folder in the views directory.
  • Below are some snippets that also need to be added.

In user.rb

This snippet is a way to make the current_user and the ip address available to all models and callbacks for the current thread.

  class << self
    def current_user=(user)
      Thread.current[:current_user] = user
    end

    def current_user
      Thread.current[:current_user]
    end

    def current_ip_address=(ip)
      Thread.current[:current_ip_address] = ip
    end

    def current_ip_address
      Thread.current[:current_ip_address]
    end
  end

In application_controller.rb

This snippet will set the current_user and ip_address for the current thread on every request.

  before_filter :set_current_user_and_ip
  def set_current_user_and_ip
    User.current_user = current_user
    User.current_ip_address = request.remote_ip
  end

How it will look

What is each file for?

_activities.html.erb

A simple helper that renders the activities

_activity.html.erb

What rails will render when you call render activity or render activities

activity.rb

The activity class, basic model with a class method to register an activity. This method is not necessary, but helps readability.

base_model.rb

The magic class. This handles some callbacks to register activities on updates, creates and destroys. Subclass this for any model you would like to track.

activities_helper.rb

Just some view helpers for keywords in the view, the icons, and using "a" vs "an"

migrations.rb

Migration to add the activity model.

see_more.js.coffee

Handles clicking the see more link in _activity.html.erb

<div class="page-header">
<h1>Recent Activity</h1>
</div>
<ul class="media-list">
<%= render activities %>
</ul>
<li class="media">
<a class="media-left" href="#">
<span class="glyphicon <%= glyphicon_class_for_action(activity.action) %>" style="font-size: 30px;"></span>
</a>
<div class="media-body">
<h4 class="media-heading">
<%= activity.owner.name %> <%= activity.action %> <%= key_word_for_activity(activity.trackable_type, current_user) %>
<small><%= distance_of_time_in_words_to_now(activity.created_at) %> ago from <%= activity.ip_address %></small>
</h4>
<% if activity.parameters.present? %>
<a href="#changes-<%= activity.id %>" data-id="<%=activity.id%>" class="see-more" data-shown="false">See More...</a>
<div class="changes hide" id="changes-<%= activity.id %>">
<% activity.parameters.each do |key, value| %>
<dl>
<dt><%= key.humanize %></dt>
<dd><%= value.humanize %></dd>
</dl>
<% end %>
</div>
<% end %>
</div>
</li>
# This is used in the views to help show the optimal view output
module ActivitiesHelper
def key_word_for_activity(trackable_type, current_user)
if ["User"].include?(trackable_type)
""
elsif current_user.role_type == trackable_type
"their profile"
else
indefinite_articlerize(trackable_type)
end
end
def glyphicon_class_for_action(action)
if action == "updated"
"glyphicon-repeat"
elsif action == "created"
"glyphicon-asterisk"
elsif action == "deleted"
"glyphicon-remove"
elsif action == "changed their password"
"glyphicon-lock"
else
"glyphicon-question-sign"
end
end
def indefinite_articlerize(params_word)
%w(a e i o u).include?(params_word[0].downcase) ? "an #{params_word}" : "a #{params_word}"
end
end
class Activity < ActiveRecord::Base
default_scope { order(created_at: :desc) }
belongs_to :owner, :polymorphic => true
belongs_to :trackable, :polymorphic => true
serialize :parameters, Hash
def self.register_activity(user, trackable, action, ip_address, params={})
act = Activity.new
act.trackable = trackable
act.owner = user
act.action = action
act.parameters = params
act.ip_address = ip_address
act.save
end
end
# Everything should extend this file that you want to track
class BaseModel < ActiveRecord::Base
self.abstract_class = true
has_one :activity, as: :trackable
before_validation(on: :update) do
if self.changes.present?
Activity.register_activity(User.current_user, self, "updated", User.current_ip_address, action_params)
end
true # don't stop the update if logging fails!
end
after_create do
Activity.register_activity(User.current_user, self, "created", User.current_ip_address)
true
end
after_destroy do
Activity.register_activity(User.current_user, self, "deleted", User.current_ip_address)
true
end
private
def action_params
changes = {}
self.changes.each do |key, change|
changes[key] = "changed the value for #{key} from \"#{change.first}\" to \"#{change.last}\"" if valid_key?(key) && change.first != change.last
end
changes
end
def valid_key?(key)
!["encrypted_password", "created_at", "updated_at"].include?(key) # add more here as you come across them.
end
end
class CreateActivities < ActiveRecord::Migration
def change
create_table :activities do |t|
t.references :trackable, polymorphic: true, index: true
t.references :owner, polymorphic: true, index: true
t.string :action
t.string :ip_address
t.text :parameters
t.timestamps
end
end
end
$(document).on "ready page:load", ->
$('.see-more').on "click", ->
id = $(this).data("id")
shown = $(this).data("shown")
changes_id = "#changes-#{id}"
if shown
$(changes_id).addClass("hide")
$(this).data("shown", false)
$(this).text("See More...")
else
$(changes_id).removeClass("hide")
$(this).data("shown", true)
$(this).text("See Less...")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment