Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to force metadata saving in case when there is no changes in model #447

Closed
holyketzer opened this issue Dec 19, 2014 · 3 comments
Closed

Comments

@holyketzer
Copy link

It isn't paper_trail issue, it's a question.

I have papertrailed model, and I use metadata fileds to save related objects snapshot changes. But if there is no changes in the main object, no versions with metadata is saved.

I need some way to take part in paper_trail's decision about save/not save the new version of object

class Movie < ActiveRecord::Base

  ...

  has_paper_trail meta: {
    related_objects: :related_objects_snapshot_as_json,
    related_objects_changes: :related_objects_snapshot_changes_as_json
  }

  ...

end
@batter
Copy link
Collaborator

batter commented Dec 19, 2014

You can always trigger a manual version creation just by mimicing what the source code is doing.

So you could make a method like this (on your model):

def generate_version!
  object_attrs = object_attrs_for_paper_trail(self)
  object_value = self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs)
  data = {
    event: 'associations_change', # or some custom event name?
    object: object_value,
    whodunnit: PaperTrail.whodunnit
  }
  send(self.class.versions_association_name).create! merge_metadata(data)
end

@holyketzer
Copy link
Author

Thank you very much for your answer.

I spent a lot of time to find the better solution for my task. I post it, probably someone will find it usefull.

I need to track not only record fields and belongs_to item but also has_many relations. Main requirement was to save all changes in one version. We use ActiveAdmin and user can do many changes with one record on one page (include changing assigned has_many objects and its properties).

I added related_objects and related_objects_changes fields to Version for storing additional data, use after_commit call back to write final version of all changes.

There is a concern which implements main logic:

module WithVersions
  extend ActiveSupport::Concern

  included do
    # Will save new version of the object
    after_commit do
      generate_version!
    end

    # Cache object changes to access it from after_commit
    after_update do
      @event = 'update'
      object_changes
    end

    # Cache all details to access it from after_commit
    before_destroy do
      @event = 'destroy'
      object_value
      object_changes
      object_metadata
    end

    after_create do
      @event = 'create'
    end
  end

  module ClassMethods
    def has_paper_trail_with_relations(&block)
      has_paper_trail on: [:nothing], meta: {
        related_objects: :related_objects_snapshot_as_json,
        related_objects_changes: :related_objects_snapshot_changes_as_json
      }

      self.class_eval do
        define_method(:related_objects_snapshot) { self.instance_eval &block }
      end
    end
  end

  def related_objects_snapshot_as_json
    related_objects_snapshot.to_json
  end

  def related_objects_snapshot_changes
    prev_version = self.versions.last
    if prev_version
      old_snapshot = JSON.parse(prev_version.related_objects).with_indifferent_access
    end

    old_snapshot ||= {}
    new_snapshot = related_objects_snapshot.with_indifferent_access

    new_snapshot.keys.reduce({}) do |diff, key|
      old_value = old_snapshot[key]
      new_value = new_snapshot[key]

      if old_value.class == new_value.class
        relation_diff = {}

        added = new_value - old_value
        relation_diff[:+] = added unless added.empty?

        removed = old_value - new_value
        relation_diff[:-] = removed unless removed.empty?

        diff[key] = relation_diff unless relation_diff.empty?
      end

      diff
    end
  end

  def related_objects_snapshot_changes_as_json
    related_objects_snapshot_changes.to_json
  end

  def object_value
    @object_value ||= begin
      object_attrs = object_attrs_for_paper_trail(self)
      self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs)
    end
  end

  def object_changes
    @object_changes ||= begin
      self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail : PaperTrail.serializer.dump(changes_for_paper_trail)
    end
  end

  def object_metadata
    @object_metadata ||= merge_metadata({})
  end

  def generate_version!
    data = {
      event: @event,
      object: object_value,
      object_changes: object_changes,
      whodunnit: PaperTrail.whodunnit,
      item_type: self.class.name,
      item_id: self.id,
    }

    if @event == 'create' && respond_to?(:created_at)
      data[PaperTrail.timestamp_field] = created_at
    elsif @event == 'update' && respond_to?(:updated_at)
      data[PaperTrail.timestamp_field] = updated_at
    end

    PaperTrail::Version.create! data.merge!(object_metadata)
  end
end

Sample of usage

class Movie < ActiveRecord::Base
  # ...
  include WithVersions

  has_paper_trail_with_relations do
    {
      cast_members: cast_members.map { |c| { role: c.role, character_name: c.character_name, person: c.person.id } },
      genres: genres.map(&:id),
      images: images.map(&:url),
    }
  end

Also there is a some lines of code for rendering extended PaperTrail::Version model in admin panel to show nice diffs.

@batter
Copy link
Collaborator

batter commented Dec 24, 2014

If you are trying to track associations, you may want to give the master branch a try, as we've just merged in #439, which contains support for tracking associations, and seems to be stable from my testing. It's going to be a little bit longer though until I can fix some tests, work on serialized attribute adjustments (they are getting removed in the newer releases of rails) and get around to cutting a release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants