Skip to content

Commit

Permalink
Merge pull request #1 from lyfeyaj/has-many-associations
Browse files Browse the repository at this point in the history
add has many associations functionality
  • Loading branch information
lyfeyaj committed Feb 26, 2014
2 parents 2144335 + 1c6f394 commit f4a459c
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 37 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ Gemfile.lock
vendor/*
.idea
.rvmrc
.tags
.tags_sorted_by_file
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# PaperTrail [![Build Status](https://img.shields.io/travis/airblade/paper_trail.svg)](https://travis-ci.org/airblade/paper_trail) [![Dependency Status](https://img.shields.io/gemnasium/airblade/paper_trail.svg)](https://gemnasium.com/airblade/paper_trail)
# PaperTrail [![Build Status](https://img.shields.io/travis/airblade/paper_trail/master.svg)](https://travis-ci.org/airblade/paper_trail) [![Dependency Status](https://img.shields.io/gemnasium/airblade/paper_trail.svg)](https://gemnasium.com/airblade/paper_trail)

PaperTrail lets you track changes to your models' data. It's good for auditing or versioning. You can see how a model looked at any stage in its lifecycle, revert it to any version, and even undelete it after it's been destroyed.

Expand Down
15 changes: 13 additions & 2 deletions lib/generators/paper_trail/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,23 @@ class InstallGenerator < ::Rails::Generators::Base
desc 'Generates (but does not run) a migration to add a versions table.'

def create_migration_file
migration_template 'create_versions.rb', 'db/migrate/create_versions.rb'
migration_template 'add_object_changes_column_to_versions.rb', 'db/migrate/add_object_changes_column_to_versions.rb' if options.with_changes?
add_paper_trail_migration('create_versions')
add_paper_trail_migration('add_object_changes_column_to_versions') if options.with_changes?
add_paper_trail_migration('create_version_associations')
add_paper_trail_migration('add_transaction_id_column_to_versions')
end

def self.next_migration_number(dirname)
::ActiveRecord::Generators::Base.next_migration_number(dirname)
end

protected
def add_paper_trail_migration(template)
migration_dir = File.expand_path('db/migrate')

if !self.class.migration_exists?(migration_dir, template)
migration_template "#{template}.rb", "db/migrate/#{template}.rb"
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class AddTransactionIdColumnToVersions < ActiveRecord::Migration
def self.up
add_column :versions, :transaction_id, :integer
add_index :versions, [:transaction_id]
end

def self.down
remove_column :versions, :transaction_id
remove_index :versions, [:transaction_id]
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class CreateVersionAssociations < ActiveRecord::Migration
def self.up
create_table :version_associations do |t|
t.integer :version_id
t.string :foreign_key_name, :null => false
t.integer :foreign_key_id
end
add_index :version_associations, [:version_id]
add_index :version_associations, [:foreign_key_name, :foreign_key_id], :name => 'index_on_foreign_key_name_and foreign_key_id'
end

def self.down
remove_index :version_associations, [:version_id]
remove_index :version_associations, [:foreign_key_name, :foreign_key_id], :name => 'index_on_foreign_key_name_and foreign_key_id'
drop_table :version_associations
end
end
23 changes: 23 additions & 0 deletions lib/paper_trail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,28 @@ def self.active_record_protected_attributes?
@active_record_protected_attributes ||= ::ActiveRecord::VERSION::MAJOR < 4 || !!defined?(ProtectedAttributes)
end

def self.transaction?
ActiveRecord::Base.connection.open_transactions > 0 || paper_trail_store[:transaction_open]
end

def self.start_transaction
paper_trail_store[:transaction_open] = true
self.transaction_id = nil
end

def self.end_transaction
paper_trail_store[:transaction_open] = false
self.transaction_id = nil
end

def self.transaction_id
paper_trail_store[:transaction_id]
end

def self.transaction_id=(id)
paper_trail_store[:transaction_id] = id
end

private

# Thread-safe hash to hold PaperTrail's data.
Expand Down Expand Up @@ -119,6 +141,7 @@ def self.configure
end

require 'paper_trail/version'
require 'paper_trail/version_association'

# Require frameworks
require 'paper_trail/frameworks/rails'
Expand Down
77 changes: 61 additions & 16 deletions lib/paper_trail/has_paper_trail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ module ClassMethods
# `:create`, `:update`, `:destroy` as desired.
# :class_name the name of a custom Version class. This class should inherit from `PaperTrail::Version`.
# :ignore an array of attributes for which a new `Version` will not be created if only they change.
# it can also aceept a Hash as an argument where the key is the attribute to ignore (a `String` or `Symbol`),
# it can also aceept a Hash as an argument where the key is the attribute to ignore (a `String` or `Symbol`),
# which will only be ignored if the value is a `Proc` which returns truthily.
# :if, :unless Procs that allow to specify conditions when to save versions for an object
# :only inverse of `ignore` - a new `Version` will be created only for these attributes if supplied
# it can also aceept a Hash as an argument where the key is the attribute to track (a `String` or `Symbol`),
# it can also aceept a Hash as an argument where the key is the attribute to track (a `String` or `Symbol`),
# which will only be counted if the value is a `Proc` which returns truthily.
# :skip fields to ignore completely. As with `ignore`, updates to these fields will not create
# a new `Version`. In addition, these fields will not be included in the serialized versions
Expand Down Expand Up @@ -74,6 +74,10 @@ def has_paper_trail(options = {})
after_create :record_create, :if => :save_version? if options_on.empty? || options_on.include?(:create)
before_update :record_update, :if => :save_version? if options_on.empty? || options_on.include?(:update)
after_destroy :record_destroy, :if => :save_version? if options_on.empty? || options_on.include?(:destroy)

# Reset the transaction id when the transaction is closed
after_commit :reset_transaction_id
after_rollback :reset_transaction_id
end

# Switches PaperTrail off for this class.
Expand All @@ -91,7 +95,7 @@ def paper_trail_on!
PaperTrail.enabled_for_model(self, true)
end

def paper_trail_on
def paper_trail_on
warn "DEPRECATED: use `paper_trail_on!` instead of `paper_trail_on`. Support for `paper_trail_on` will be removed in PaperTrail 3.1"
self.paper_trail_on!
end
Expand Down Expand Up @@ -216,6 +220,16 @@ def without_versioning(method = nil)
self.class.paper_trail_on! if paper_trail_was_enabled
end

# Utility method for reifying. Anything executed inside the block will appear like a new record
def appear_as_new_record
instance_eval {
alias :old_new_record? :new_record?
alias :new_record? :present?
}
yield
instance_eval { alias :new_record? :old_new_record? }
end

# Mimicks behavior of `touch` method from `ActiveRecord::Persistence`, but generates a version
#
# TODO: lookinto leveraging the `after_touch` callback from `ActiveRecord` to allow the
Expand All @@ -241,31 +255,37 @@ def source_version
def record_create
if paper_trail_switched_on?
data = {
:event => paper_trail_event || 'create',
:whodunnit => PaperTrail.whodunnit
:event => paper_trail_event || 'create',
:whodunnit => PaperTrail.whodunnit,
:transaction_id => PaperTrail.transaction_id
}

if changed_notably? and self.class.paper_trail_version_class.column_names.include?('object_changes')
data[:object_changes] = self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail :
PaperTrail.serializer.dump(changes_for_paper_trail)
end
send(self.class.versions_association_name).create! merge_metadata(data)
version = send(self.class.versions_association_name).create! merge_metadata(data)
set_transaction_id(version)
save_associations(version)
end
end

def record_update
if paper_trail_switched_on? && changed_notably?
object_attrs = object_attrs_for_paper_trail(item_before_change)
data = {
:event => paper_trail_event || 'update',
:object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
:whodunnit => PaperTrail.whodunnit
:event => paper_trail_event || 'update',
:object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
:whodunnit => PaperTrail.whodunnit,
:transaction_id => PaperTrail.transaction_id
}
if self.class.paper_trail_version_class.column_names.include?('object_changes')
data[:object_changes] = self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail :
PaperTrail.serializer.dump(changes_for_paper_trail)
end
send(self.class.versions_association_name).build merge_metadata(data)
version = send(self.class.versions_association_name).create merge_metadata(data)
set_transaction_id(version)
save_associations(version)
end
end

Expand All @@ -279,17 +299,42 @@ def record_destroy
if paper_trail_switched_on? and not new_record?
object_attrs = object_attrs_for_paper_trail(item_before_change)
data = {
:item_id => self.id,
:item_type => self.class.base_class.name,
:event => paper_trail_event || 'destroy',
:object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
:whodunnit => PaperTrail.whodunnit
:item_id => self.id,
:item_type => self.class.base_class.name,
:event => paper_trail_event || 'destroy',
:object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
:whodunnit => PaperTrail.whodunnit,
:transaction_id => PaperTrail.transaction_id
}
self.class.paper_trail_version_class.create merge_metadata(data)
version = self.class.paper_trail_version_class.create merge_metadata(data)
send(self.class.versions_association_name).send :load_target
set_transaction_id(version)
save_associations(version)
end
end

def save_associations(version)
self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
PaperTrail::VersionAssociation.create(
:version_id => version.id,
:foreign_key_name => assoc.foreign_key,
:foreign_key_id => self.send(assoc.foreign_key)
)
end
end

def set_transaction_id(version)
if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
PaperTrail.transaction_id = version.id
version.transaction_id = version.id
version.save
end
end

def reset_transaction_id
PaperTrail.transaction_id = nil
end

def merge_metadata(data)
# First we merge the model-level metadata in `meta`.
paper_trail_options[:meta].each do |k,v|
Expand Down
7 changes: 7 additions & 0 deletions lib/paper_trail/version_association.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module PaperTrail
class VersionAssociation < ActiveRecord::Base
belongs_to :version

attr_accessible :version_id, :foreign_key_name, :foreign_key_id if PaperTrail.active_record_protected_attributes?
end
end
Loading

0 comments on commit f4a459c

Please sign in to comment.