'How to show in a view page a list of updates made to a record in ActiveRecord

I am making a simple contacts list app in Ruby. On the show view for each contact, I want to display the basic information (first_name, last_name, email and phone) but also if there have been any changes or updates to that contact. For example, if they changed their email address from [email protected] to [email protected] and this change was made on 27/03/2022 I want a list of this information to also be displayed on the show page.

I guess it is something to do with including ActiveModel::Dirty or a callback but not too sure how to implement any of this!

This is my Contact.rb model file

# frozen_string_literal: true

class Contact < ApplicationRecord
  before_validation :normalize_first_name, on: :create
  before_validation :normalize_last_name, on: :create
  # after_save :print_changes
  # before_update :check_for_changes
  # after_update :check_for_previous_changes

  validates_presence_of :first_name, :last_name, :email, :phone
  validates :phone, numericality: { only_integer: true }
  validates :email, uniqueness: true
  validates :email,
            format: { with: /\A(.+)@(.+)\z/, message: 'Email invalid' },
            uniqueness: { case_sensitive: false },
            length: { minimum: 4, maximum: 95 }

  private

  def normalize_first_name
    self.first_name = first_name.downcase.titleize
  end

  def normalize_last_name
    self.last_name = last_name.downcase.titleize
  end

  # def check_for_changes
  #   puts changes # => {"name"=>["Nimish Gupta", "Nimish Mahajan"], "updated_at"=>[Tue, 20 Nov 2018 00:02:14 PST -08:00, Tue, 20 Nov 2018 00:06:15 PST -08:00]}
  #   puts previous_changes # => {} At this point this will be empty beacuse changes are not made to DB yet
  # end

  # def check_for_previous_changes
  #   # but you can make use of previous_changes method to know what change has occurred.
  #   puts previous_changes # => {"name"=>["Nimish Gupta", "Nimish Mahajan"], "updated_at"=>[Tue, 20 Nov 2018 00:06:15 PST -08:00, Tue, 20 Nov 2018 00:08:07 PST -08:00]}
  # end
end

This is the Contact controller

class ContactsController < ApplicationController
  before_action :set_contact, only: %i[show edit update destroy]

  # GET /contacts or /contacts.json
  def index
    @contacts = Contact.all
    @contact = Contact.new
  end

  # GET /contacts/1 or /contacts/1.json
  def show
  end

  # GET /contacts/new
  def new
    @contact = Contact.new
  end

  # GET /contacts/1/edit
  def edit; end

  # POST /contacts or /contacts.json
  def create
    @contact = Contact.new(contact_params)

    respond_to do |format|
      if @contact.save
        format.html { redirect_to contact_url(@contact), notice: 'Contact was successfully created.' }
        format.json { render :show, status: :created, location: @contact }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @contact.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /contacts/1 or /contacts/1.json
  def update
    respond_to do |format|
      if @contact.update(contact_params)
        format.html { redirect_to contact_url(@contact), notice: 'Contact was successfully updated.' }
        format.json { render :show, status: :ok, location: @contact }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @contact.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /contacts/1 or /contacts/1.json
  def destroy
    @contact.destroy

    respond_to do |format|
      format.html { redirect_to contacts_url, notice: 'Contact was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private

  # Use callbacks to share common setup or constraints between actions.
  def set_contact
    @contact = Contact.find(params[:id])
  end

  # Only allow a list of trusted parameters through.
  def contact_params
    params.require(:contact).permit(:first_name, :last_name, :email, :phone)
  end
end

And the Show page where I want the changes to be displayed

<p id="notice"><%= notice %></p>

<h1>This is the show page for each contact</h1>
<p>
  <strong>First name:</strong>
  <%= @contact.first_name %>
</p>

<p>
  <strong>Last name:</strong>
  <%= @contact.last_name %>
</p>

<p>
  <strong>Email:</strong>
  <%= @contact.email %>
</p>

<p>
  <strong>Phone:</strong>
  <%= @contact.phone %>
</p>

<%= link_to 'Edit', edit_contact_path(@contact) %> |
<%= link_to 'Back', contacts_path %>

<h1>Contact history</h1>

<%# <%= check_for_changes(@contact) %>


<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Updated</th>
      <th>Previous entry</th>
    </tr>
  </thead>

  <tbody>
    <%# <% @contacts.each do |contact| > %>
      <tr>
        <td>Email</td>
        <td>27/03/2022</td>
        <td>[email protected]</td>
        <%# <td><%= contact.first_name ></td> %>
        <%# <td><%= contact.last_name ></td> %>
        <%# <td><%= contact.email ></td> %>
      </tr>
    <%# <% end > %>
  </tbody>
</table>


Solution 1:[1]

ActiveModel::Dirty only allows you to track changes within the same request the changes are made. Once the contact is stored to the database with the new changes and the request ends, there is no built in method to get the old values anymore.

You will have to build a solution yourself. You will need a model that can store all the changes that were made, which belong to your contact. Let's call that ContactChange from now on.

In order to create the contact change any time the contact is changed, you could use the ActiveModel::Dirty methods and put it in an after_save callback, or you could create a service object to both update the contact and create a contact change.

Or if you just want an out of the box solution, there are a few gems. For example paper trail.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Siim Liiser