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

Solution for tracking has_and_belongs_to_many relations #215

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions lib/mongoid/history/trackable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def track_history(options = {})
end

include MyInstanceMethods
include HasAndBelongsToManyMethods
extend SingletonMethods

delegate :history_trackable_options, to: 'self.class'
Expand Down Expand Up @@ -267,6 +268,12 @@ def transform_changes(changes)
[original, modified]
end

def increment_current_version
current_version = (send(history_trackable_options[:version_field]) || 0) + 1
send("#{history_trackable_options[:version_field]}=", current_version)
current_version
end

protected

def track_history_for_action?(action)
Expand All @@ -275,8 +282,7 @@ def track_history_for_action?(action)

def track_history_for_action(action)
if track_history_for_action?(action)
current_version = (send(history_trackable_options[:version_field]) || 0) + 1
send("#{history_trackable_options[:version_field]}=", current_version)
current_version = increment_current_version
last_track = self.class.tracker_class.create!(history_tracker_attributes(action.to_sym).merge(version: current_version, action: action.to_s, trackable: self))
end

Expand All @@ -294,6 +300,35 @@ def track_history_for_action(action)
end
end

module HasAndBelongsToManyMethods
def track_has_and_belongs_to_many(related)
# skip for new records (track_create will capture assignment) and when track updates disabled
return true if new_record? || !track_history? || !history_trackable_options[:track_update]
metadata = reflect_on_all_associations(:has_and_belongs_to_many).find { |m| m.class_name == related.class.name }

related_id = related.id
original_ids = send(metadata.key.to_sym)
modified_ids = if original_ids.include?(related_id)
original_ids.reject { |id| id == related_id }
else
original_ids + [related_id]
end

modified = { metadata.key => modified_ids }
original = { metadata.key => original_ids }
action = :update
current_version = increment_current_version
self.class.tracker_class.create!(
history_tracker_attributes(action.to_sym)
.merge(version: current_version,
action: action.to_s,
original: original,
modified: modified,
trackable: self)
)
end
end

module EmbeddedMethods
# Indicates whether there is an Embedded::One relation for the given embedded field.
#
Expand Down
178 changes: 178 additions & 0 deletions spec/integration/has_and_belongs_to_many_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
require 'spec_helper'

describe Mongoid::History do
before :all do
class Tag
include Mongoid::Document

field :title
has_and_belongs_to_many :posts
end
end

describe 'track' do
before :all do
class Post
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::History::Trackable

field :title
field :body
has_and_belongs_to_many :tags, before_add: :track_has_and_belongs_to_many, before_remove: :track_has_and_belongs_to_many
track_history on: %i[fields]
end
end

let(:tag) { Tag.create! }

describe 'on creation' do
let(:post) { Post.create!(tags: [tag]) }

it 'should create track' do
expect(post.history_tracks.count).to eq(1)
end

it 'should assign tag_ids on modified' do
expect(post.history_tracks.first.modified).to include('tag_ids' => [tag.id])
end

it 'should be empty on original' do
expect(post.history_tracks.first.original).to eq({})
end
end

describe 'on add' do
let(:post) { Post.create!(tags: [tag]) }
let(:tag2) { Tag.create! }
before { post.tags << tag2 }

# this just verifies that post is updated above
it 'should update tags' do
expect(post.reload.tags).to eq([tag, tag2])
end

it 'should create track' do
expect(post.history_tracks.count).to eq(2)
end

it 'should assign tag_ids on modified' do
expect(post.history_tracks.last.modified).to include('tag_ids' => [tag.id, tag2.id])
end

it 'should assign tag_ids on original' do
expect(post.history_tracks.last.original).to include('tag_ids' => [tag.id])
end
end

describe 'on remove' do
let(:post) { Post.create!(tags: [tag]) }
before { post.tags = [] }

# this just verifies that post is updated above
it 'should update tags' do
expect(post.reload.tags).to eq([])
end

it 'should create two tracks' do
expect(post.history_tracks.count).to eq(2)
end

it 'should assign empty tag_ids on modified' do
expect(post.history_tracks.last.modified).to include('tag_ids' => [])
end

it 'should assign tag_ids on original' do
expect(post.history_tracks.last.original).to include('tag_ids' => [tag.id])
end
end

describe 'on reassign' do
let(:post) { Post.create!(tags: [tag]) }
let(:tag2) { Tag.create! }
before { post.tags = [tag2] }

# this just verifies that post is updated above
it 'should update tags' do
expect(post.reload.tags).to eq([tag2])
end

it 'should create three tracks' do
# 1. tags: [tag]
# 2. tags: []
# 3. tags: [tag2]
expect(post.history_tracks.count).to eq(3)
end

it 'should assign tag_ids on modified' do
expect(post.history_tracks.last.modified).to include('tag_ids' => [tag2.id])
end

it 'should assign empty tag_ids on original' do
expect(post.history_tracks.last.original).to include('tag_ids' => [])
end
end

after :all do
Object.send(:remove_const, :Post)
end
end

describe 'not track' do
let!(:post) { Post.create! }

context 'track_update: false' do
before :all do
class Post
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::History::Trackable

field :title
field :body
has_and_belongs_to_many :tags, before_add: :track_has_and_belongs_to_many, before_remove: :track_has_and_belongs_to_many
track_history on: %i[fields], track_update: false
end
end

it 'should not create track' do
expect { post.tags = [Tag.create!] }.not_to change(Tracker, :count)
end

after :all do
Object.send(:remove_const, :Post)
end
end

context '#disable_tracking' do
before :all do
class Post
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::History::Trackable

field :title
field :body
has_and_belongs_to_many :tags, before_add: :track_has_and_belongs_to_many, before_remove: :track_has_and_belongs_to_many
track_history on: %i[fields]
end
end

it 'should not create track' do
expect do
Post.disable_tracking do
post.tags = [Tag.create!]
end
end.not_to change(Tracker, :count)
end

after :all do
Object.send(:remove_const, :Post)
end
end
end

after :all do
Object.send(:remove_const, :Tag)
end
end