Skip to content

Commit

Permalink
Condensing code for Hash args to the :ignore and :only options; also …
Browse files Browse the repository at this point in the history
…clarifying README
  • Loading branch information
Ben Atkins committed Sep 19, 2013
1 parent 567f21b commit 22dd0cc
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 48 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 3.0.0 (Unreleased)

- [#273](https://github.com/airblade/paper_trail/pull/273) - Make the `only` and `ignore` options accept `Hash` arguments;
allows for conditional tracking.
- [#264](https://github.com/airblade/paper_trail/pull/264) - Allow unwrapped symbol to be passed in to the `on` option.
- [#224](https://github.com/airblade/paper_trail/issues/224)/[#236](https://github.com/airblade/paper_trail/pull/236) -
Fixed compatibility with [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on).
Expand Down
40 changes: 29 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ You can ignore changes to certain attributes like this:

```ruby
class Article < ActiveRecord::Base
has_paper_trail :ignore => [:title, :rating => Proc.new { |obj| obj.raiting == 0 } ]
has_paper_trail :ignore => [:title, :rating]
end
```

Expand All @@ -277,25 +277,22 @@ This means that changes to just the `title` or `rating` will not store another v
```ruby
>> a = Article.create
>> a.versions.length # 1
>> a.update_attributes :title => 'My Title', :rating => 0
>> a.versions.length # 1
>> a.update_attributes :title => 'My Title', :rating => 3
>> a.versions.length # 2
>> a.previous_version.raiting # nil
>> a.versions.length # 1
>> a.update_attributes :title => 'Greeting', :content => 'Hello'
>> a.versions.length # 3
>> a.versions.length # 2
>> a.previous_version.title # 'My Title'
```

Or, you can specify a list of all attributes you care about:

```ruby
class Article < ActiveRecord::Base
has_paper_trail :only => [:title, :author => Proc.new { |obj| obj.author.present? }]
has_paper_trail :only => [:title]
end
```

This means that only changes to the `title` and non-empty `author` will save a version of the article:
This means that only changes to the `title` will save a version of the article:

```ruby
>> a = Article.create
Expand All @@ -305,11 +302,31 @@ This means that only changes to the `title` and non-empty `author` will save a v
>> a.update_attributes :content => 'Hello'
>> a.versions.length # 2
>> a.previous_version.content # nil
>> a.update_attributes :author => 'Me'
```

The `:ignore` and `:only` options can also accept `Hash` arguments, where the :

```ruby
class Article < ActiveRecord::Base
has_paper_trail :only => [:title => Proc.new { |obj| !obj.title.blank? } ]
end
```

This means that if the `title` is not blank, then only changes to the `title` will save a version of the article:

```ruby
>> a = Article.create
>> a.versions.length # 1
>> a.update_attributes :content => 'Hello'
>> a.versions.length # 2
>> a.update_attributes :title => 'My Title'
>> a.versions.length # 3
>> a.update_attributes :author => ''
>> a.update_attributes :content => 'Hai'
>> a.versions.length # 3
>> a.previous_version.author # 'Me'
>> a.previous_version.content # "Hello"
>> a.update_attributes :title => 'Dif Title'
>> a.versions.length # 4
>> a.previous_version.content # "Hai"
```

Passing both `:ignore` and `:only` options will result in the article being saved if a changed attribute is included in `:only` but not in `:ignore`.
Expand Down Expand Up @@ -973,6 +990,7 @@ Many thanks to:
* [Bradley Priest](https://github.com/bradleypriest)
* [David Butler](https://github.com/dwbutler)
* [Paul Belt](https://github.com/belt)
* [Vlad Bokov](https://github.com/razum2um)


## Inspirations
Expand Down
52 changes: 17 additions & 35 deletions lib/paper_trail/has_paper_trail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ module ClassMethods
# `:create`, `:update`, `:destroy` as desired.
# :class_name the name of a custom Version class. This class should inherit from Version.
# :ignore an array of attributes for which a new `Version` will not be created if only they change.
# it can also has a Hash as an item: the key is the attribute to ignore, the value - a Proc
# given a current state of object before save, only if it results in true - attribute will be ignored

This comment has been minimized.

Copy link
@razum2um

razum2um Sep 19, 2013

thanks for the good english
i'm not a native speaker

This comment has been minimized.

Copy link
@batter

batter Sep 19, 2013

Collaborator

No worries about that mate, it was a mouthful to try to fit into one sentence 😁

# 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 has a Hash as an item: the key is the attribute to track, the value - a Proc
# given a current state of object before save, only if it results in true - attribute will create a version
# 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
# of the object whenever a new `Version` is created.
Expand Down Expand Up @@ -50,15 +50,7 @@ def has_paper_trail(options = {})

[:ignore, :skip, :only].each do |k|
paper_trail_options[k] =
([paper_trail_options[k]].flatten.compact || []).map do |attr|
if attr.is_a? Hash
Hash[attr.map do |attr_name, condition|
[attr_name.to_s, condition]
end]
else
attr.to_s
end
end
([paper_trail_options[k]].flatten.compact || []).map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }

This comment has been minimized.

Copy link
@razum2um

razum2um Sep 19, 2013

cool. i thought there was no dependency on activesupport

This comment has been minimized.

Copy link
@batter

batter Sep 19, 2013

Collaborator

There is a dependency on ActiveRecord, which has a dependency on ActiveSupport.

end

paper_trail_options[:meta] ||= {}
Expand Down Expand Up @@ -285,7 +277,7 @@ def item_before_change
end

def object_to_string(object)
_attrs = object.attributes.except(*self.class.paper_trail_options[:skip]).tap do |attributes|
_attrs = object.attributes.except(*self.paper_trail_options[:skip]).tap do |attributes|
self.class.serialize_attributes_for_paper_trail attributes
end
PaperTrail.serializer.dump(_attrs)
Expand All @@ -296,31 +288,21 @@ def changed_notably?
end

def notably_changed
only = []
self.class.paper_trail_options[:only].each do |attr|
if attr.is_a? Hash
attr.each do |attr_name, condition|
only << attr_name if condition.respond_to?(:call) && condition.call(self)
end
else
only << attr
end
only = self.paper_trail_options[:only].dup
# remove Hash arguments and then evaluate whether the attributes (the keys of the hash) should also get pushed into the collection
only.delete_if do |obj|
obj.is_a?(Hash) && obj.each { |attr, condition| only << attr if condition.respond_to?(:call) && condition.call(self) }
end
only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
end

def changed_and_not_ignored
ignore = []
self.class.paper_trail_options[:ignore].each do |attr|
if attr.is_a? Hash
attr.each do |attr_name, condition|
ignore << attr_name if condition.respond_to?(:call) && condition.call(self)
end
else
ignore << attr
end
ignore = self.paper_trail_options[:ignore].dup
# remove Hash arguments and then evaluate whether the attributes (the keys of the hash) should also get pushed into the collection
ignore.delete_if do |obj|
obj.is_a?(Hash) && obj.each { |attr, condition| ignore << attr if condition.respond_to?(:call) && condition.call(self) }
end
skip = self.class.paper_trail_options[:skip]
skip = self.paper_trail_options[:skip]
changed - ignore - skip
end

Expand All @@ -329,8 +311,8 @@ def paper_trail_switched_on?
end

def save_version?
if_condition = self.class.paper_trail_options[:if]
unless_condition = self.class.paper_trail_options[:unless]
if_condition = self.paper_trail_options[:if]
unless_condition = self.paper_trail_options[:unless]
(if_condition.blank? || if_condition.call(self)) && !unless_condition.try(:call, self)
end
end
Expand Down
4 changes: 2 additions & 2 deletions test/dummy/app/models/article.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Article < ActiveRecord::Base
has_paper_trail :ignore => [:title, { :abstract => Proc.new { |obj| ['ignore abstract', 'Other abstract'].include? obj.abstract } }],
:only => [:content, { :abstract => Proc.new { |obj| obj.abstract.present? } }],
has_paper_trail :ignore => [:title, :abstract => Proc.new { |obj| ['ignore abstract', 'Other abstract'].include? obj.abstract } ],

This comment has been minimized.

Copy link
@razum2um

razum2um Sep 19, 2013

I had strange test failures without braces. you see it again
https://travis-ci.org/airblade/paper_trail/jobs/11567404

This comment has been minimized.

Copy link
@batter

batter Sep 19, 2013

Collaborator

Hmm, seems like it's only an issue on Ruby18. But yes, I'll wrap it back up in braces to make it pass on Travis I guess.

:only => [:content, :abstract => Proc.new { |obj| obj.abstract.present? } ],
:skip => [:file_upload],
:meta => {
:answer => 42,
Expand Down

0 comments on commit 22dd0cc

Please sign in to comment.