From 4a92ed7ea584583583e788d392071053068eba3d Mon Sep 17 00:00:00 2001 From: Ben Atkins Date: Wed, 7 Jan 2015 16:57:45 -0500 Subject: [PATCH] close #420; Add VersionConcern#where_object_changes --- CHANGELOG.md | 6 ++-- README.md | 3 ++ lib/paper_trail/serializers/json.rb | 15 ++++++-- lib/paper_trail/serializers/yaml.rb | 12 +++++-- lib/paper_trail/version_concern.rb | 13 +++++++ spec/models/version_spec.rb | 54 +++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 634f3a48..dce6bf95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,18 @@ PaperTrail::Rails::Engine.eager_load! ``` -Also +*Also* If you depend on the `RSpec` or `Cucumber` helpers, you will need to [manually load them into your test helper](https://github.com/airblade/paper_trail#testing). + - [#440](https://github.com/airblade/paper_trail/pull/440) - `versions` association should clear/reload after a transaction rollback. - [#439](https://github.com/airblade/paper_trail/pull/439) / [#12](https://github.com/airblade/paper_trail/issues/12) - Support for versioning of associations (Has Many, Has One, HABTM, etc.) - - [#440](https://github.com/airblade/paper_trail/pull/440) - `versions` association should clear/reload after a transaction rollback. - [#438](https://github.com/airblade/paper_trail/issues/438) - `Model.paper_trail_enabled_for_model?` should return `false` if `has_paper_trail` has not been declared on the class. - [#427](https://github.com/airblade/paper_trail/pull/427) - Fix `reify` method in context of model where a column has been removed. + - [#420](https://github.com/airblade/paper_trail/issues/420) - Add `VersionConcern#where_object_changes` instance method; + acts as a helper for querying against the `object_changes` column in versions table. - [#416](https://github.com/airblade/paper_trail/issues/416) - Added a `config` option for enabling/disabling utilization of `serialized_attributes` for `ActiveRecord`, necessary because `serialized_attributes` has been deprecated in `ActiveRecord` version `4.2` and will be removed in version `5.0` diff --git a/README.md b/README.md index 9b2af85b..bc1a8e0e 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,9 @@ version.event # Query versions objects by attributes. PaperTrail::Version.where_object(attr1: val1, attr2: val2) + +# Query versions object_changes field by attributes (requires [`object_changes`](https://github.com/airblade/paper_trail#diffing-versions) column on versions table) +PaperTrail::Version.where_object_changes(attr1: val1) ``` In your controllers you can override these methods: diff --git a/lib/paper_trail/serializers/json.rb b/lib/paper_trail/serializers/json.rb index 99c260de..0b8ce1d0 100644 --- a/lib/paper_trail/serializers/json.rb +++ b/lib/paper_trail/serializers/json.rb @@ -13,8 +13,8 @@ module PaperTrail ActiveSupport::JSON.encode object end - # Returns a SQL condition to be used to match the given field and value in - # the serialized object. + # Returns a SQL condition to be used to match the given field and value + # in the serialized object def where_object_condition(arel_field, field, value) # Convert to JSON to handle strings and nulls correctly. json_value = value.to_json @@ -31,6 +31,17 @@ module PaperTrail arel_field.matches("%\"#{field}\":#{json_value}%") end end + + # Returns a SQL condition to be used to match the given field and value + # in the serialized object_changes + def where_object_changes_condition(arel_field, field, value) + # Convert to JSON to handle strings and nulls correctly. + json_value = value.to_json + + # Need to check first (before) and secondary (after) fields + arel_field.matches("%\"#{field}\":[#{json_value},%"). + or(arel_field.matches("%\"#{field}\":[%,#{json_value}]%")) + end end end end diff --git a/lib/paper_trail/serializers/yaml.rb b/lib/paper_trail/serializers/yaml.rb index f2cb77bd..46ae92d1 100644 --- a/lib/paper_trail/serializers/yaml.rb +++ b/lib/paper_trail/serializers/yaml.rb @@ -13,11 +13,19 @@ module PaperTrail ::YAML.dump object end - # Returns a SQL condition to be used to match the given field and value in - # the serialized object. + # Returns a SQL condition to be used to match the given field and value + # in the serialized object def where_object_condition(arel_field, field, value) arel_field.matches("%\n#{field}: #{value}\n%") end + + # Returns a SQL condition to be used to match the given field and value + # in the serialized object_changes + def where_object_changes_condition(arel_field, field, value) + # Need to check first (before) and secondary (after) fields + arel_field.matches("%\n#{field}:\n- #{value}\n%"). + or(arel_field.matches("%\n#{field}:\n- %\n- #{value}\n%")) + end end end end diff --git a/lib/paper_trail/version_concern.rb b/lib/paper_trail/version_concern.rb index 317a5c41..0cc19766 100644 --- a/lib/paper_trail/version_concern.rb +++ b/lib/paper_trail/version_concern.rb @@ -88,6 +88,19 @@ module PaperTrail where(where_conditions) end + def where_object_changes(args = {}) + raise ArgumentError, 'expected to receive a Hash' unless args.is_a?(Hash) + arel_field = arel_table[:object_changes] + + where_conditions = args.map do |field, value| + PaperTrail.serializer.where_object_changes_condition(arel_field, field, value) + end.reduce do |condition1, condition2| + condition1.and(condition2) + end + + where(where_conditions) + end + def primary_key_is_int? @primary_key_is_int ||= columns_hash[primary_key].type == :integer rescue diff --git a/spec/models/version_spec.rb b/spec/models/version_spec.rb index 1cd48321..9d3d7b32 100644 --- a/spec/models/version_spec.rb +++ b/spec/models/version_spec.rb @@ -108,6 +108,60 @@ describe PaperTrail::Version, :type => :model do end end end + + describe '#where_object_changes' do + it { expect(PaperTrail::Version).to respond_to(:where_object_changes) } + + context "invalid arguments" do + it "should raise an error" do + expect { PaperTrail::Version.where_object_changes(:foo) }.to raise_error(ArgumentError) + expect { PaperTrail::Version.where_object_changes([]) }.to raise_error(ArgumentError) + end + end + + context "valid arguments", :versioning => true do + let(:widget) { Widget.new } + let(:name) { Faker::Name.first_name } + let(:int) { rand(10) + 1 } + + before do + widget.update_attributes!(:name => name, :an_integer => 0) + widget.update_attributes!(:name => 'foobar', :an_integer => 100) + widget.update_attributes!(:name => Faker::Name.last_name, :an_integer => int) + end + + context "`serializer == YAML`" do + specify { expect(PaperTrail.serializer).to be PaperTrail::Serializers::YAML } + + it "should be able to locate versions according to their `object_changes` contents" do + expect(PaperTrail::Version.where_object_changes(:name => name)).to eq(widget.versions[0..1]) + expect(PaperTrail::Version.where_object_changes(:an_integer => 100)).to eq(widget.versions[1..2]) + expect(PaperTrail::Version.where_object_changes(:an_integer => int)).to eq([widget.versions.last]) + end + + it "should be able to handle queries for multiple attributes" do + expect(PaperTrail::Version.where_object_changes(:an_integer => 100, :name => 'foobar')).to eq(widget.versions[1..2]) + end + end + + context "`serializer == JSON`" do + before(:all) { PaperTrail.serializer = PaperTrail::Serializers::JSON } + specify { expect(PaperTrail.serializer).to be PaperTrail::Serializers::JSON } + + it "should be able to locate versions according to their `object_changes` contents" do + expect(PaperTrail::Version.where_object_changes(:name => name)).to eq(widget.versions[0..1]) + expect(PaperTrail::Version.where_object_changes(:an_integer => 100)).to eq(widget.versions[1..2]) + expect(PaperTrail::Version.where_object_changes(:an_integer => int)).to eq([widget.versions.last]) + end + + it "should be able to handle queries for multiple attributes" do + expect(PaperTrail::Version.where_object_changes(:an_integer => 100, :name => 'foobar')).to eq(widget.versions[1..2]) + end + + after(:all) { PaperTrail.serializer = PaperTrail::Serializers::YAML } + end + end + end end end end