-
Notifications
You must be signed in to change notification settings - Fork 879
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
Added has_and_belongs_to_many reification #771
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ module PaperTrail | |
module Model | ||
def self.included(base) | ||
base.send :extend, ClassMethods | ||
base.send :attr_accessor, :paper_trail_habtm | ||
end | ||
|
||
# :nodoc: | ||
|
@@ -46,6 +47,12 @@ module ClassMethods | |
# the instance was reified from. Default is `:version`. | ||
# - :save_changes - Whether or not to save changes to the object_changes | ||
# column if it exists. Default is true | ||
# - :join_tables - If the model has a has_and_belongs_to_many relation | ||
# with an unpapertrailed model, passing the name of the association to | ||
# the join_tables option will paper trail the join table but not save | ||
# the other model, allowing reification of the association but with the | ||
# other models latest state (if the other model is paper trailed, this | ||
# option does nothing) | ||
# | ||
def has_paper_trail(options = {}) | ||
options[:on] ||= [:create, :update, :destroy] | ||
|
@@ -57,6 +64,42 @@ def has_paper_trail(options = {}) | |
setup_model_for_paper_trail(options) | ||
|
||
setup_callbacks_from_options options[:on] | ||
|
||
setup_callbacks_for_habtm options[:join_tables] | ||
end | ||
|
||
def update_for_callback(name, callback, model, assoc) | ||
model.paper_trail_habtm ||= {} | ||
model.paper_trail_habtm.reverse_merge!(name => { removed: [], added: [] }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not do this? It seems easier to understand model.paper_trail_habtm[name] ||= {removed: [], added: []} |
||
case callback | ||
when :before_add | ||
model.paper_trail_habtm[name][:added] |= [assoc.id] | ||
model.paper_trail_habtm[name][:removed] -= [assoc.id] | ||
when :before_remove | ||
model.paper_trail_habtm[name][:removed] |= [assoc.id] | ||
model.paper_trail_habtm[name][:added] -= [assoc.id] | ||
end | ||
end | ||
|
||
attr_reader :paper_trail_save_join_tables | ||
|
||
def setup_callbacks_for_habtm(join_tables) | ||
@paper_trail_save_join_tables = Array.wrap(join_tables) | ||
# Adds callbacks to record changes to habtm associations such that on | ||
# save the previous version of the association (if changed) can be | ||
# interpreted | ||
reflect_on_all_associations(:has_and_belongs_to_many). | ||
reject { |a| paper_trail_options[:skip].include?(a.name.to_s) }. | ||
each do |a| | ||
added_callback = lambda do |*args| | ||
update_for_callback(a.name, :before_add, args[-2], args.last) | ||
end | ||
removed_callback = lambda do |*args| | ||
update_for_callback(a.name, :before_remove, args[-2], args.last) | ||
end | ||
send(:"before_add_for_#{a.name}").send(:<<, added_callback) | ||
send(:"before_remove_for_#{a.name}").send(:<<, removed_callback) | ||
end | ||
end | ||
|
||
def setup_model_for_paper_trail(options = {}) | ||
|
@@ -442,6 +485,11 @@ def record_destroy | |
# Saves associations if the join table for `VersionAssociation` exists. | ||
def save_associations(version) | ||
return unless PaperTrail.config.track_associations? | ||
save_associations_belongs_to(version) | ||
save_associations_has_and_belongs_to_many(version) | ||
end | ||
|
||
def save_associations_belongs_to(version) | ||
self.class.reflect_on_all_associations(:belongs_to).each do |assoc| | ||
assoc_version_args = { | ||
version_id: version.id, | ||
|
@@ -463,6 +511,27 @@ def save_associations(version) | |
end | ||
end | ||
|
||
def save_associations_has_and_belongs_to_many(version) | ||
# Use the :added and :removed keys to extrapolate the HABTM associations | ||
# to before any changes were made | ||
self.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |a| | ||
next unless | ||
self.class.paper_trail_save_join_tables.include?(a.name) || | ||
a.klass.paper_trail_enabled_for_model? | ||
assoc_version_args = { | ||
version_id: version.id, | ||
foreign_key_name: a.name | ||
} | ||
assoc_ids = | ||
send(a.name).to_a.map(&:id) + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
(@paper_trail_habtm.try(:[], a.name).try(:[], :removed) || []) - | ||
(@paper_trail_habtm.try(:[], a.name).try(:[], :added) || []) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think of this? history = @paper_trail_habtm || {}
history = history[a.name] || Hash.new([])
assoc_ids = send(a.name).map(&:id) + history[:removed] - history[:added] |
||
assoc_ids.each do |id| | ||
PaperTrail::VersionAssociation.create(assoc_version_args.merge(foreign_key_id: id)) | ||
end | ||
end | ||
end | ||
|
||
def reset_transaction_id | ||
PaperTrail.transaction_id = nil | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
class BarHabtm < ActiveRecord::Base | ||
has_and_belongs_to_many :foo_habtms | ||
has_paper_trail | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
class FooHabtm < ActiveRecord::Base | ||
has_and_belongs_to_many :bar_habtms | ||
has_paper_trail | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure that's the case? I just read through the code and don't see where that would happen. It seems like if one side of the association has
join_tables
set, and the other side of the association has paper_trail enabled withoutjoin_tables
, this code will correctly load the versions for both sides of the association.Perhaps you meant to say that there's no point enabling
join_tables
on both models?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I must have misunderstood -- HABTM models are tracked automatically, and this option exists to disable that.
I'm curious why someone would want to do that, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, if both models are paper trailed then the join table is also paper trailed, allowing a complete history of the models and associations. If only one model is paper trailed, and the other is not, then by default nothing is saved in relation to both models. However, by passing the
join_tables
option, the association changes are saved. You would only ever needjoin_tables
when you want to paper trail an association but not the associated model. If you take a look atsave_associations_has_and_belongs_to_many
you can see the logic behind the saving.