audited
audited copied to clipboard
Associated audits with HABTM relation
I've got model user
and role
class User < ApplicationRecord
rolify strict: true
has_many :roles, through: :users_roles
has_associated_audits
class Role < ApplicationRecord
has_and_belongs_to_many :users, join_table: :users_roles
audited associated_with: :users, join_table: :users_roles
When I create a new role, I've got the error:
2.4.4 :373 > User.first.add_role Role.pi, ProjectRequest.find(319)
User Load (0.7ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
ProjectRequest Load (0.6ms) SELECT `project_requests`.* FROM `project_requests` WHERE `project_requests`.`id` = 319 LIMIT 1
Role Load (0.6ms) SELECT `roles`.* FROM `roles` WHERE `roles`.`name` = 'pi' AND `roles`.`resource_type` = 'ProjectRequest' AND `roles`.`resource_id` = 319 ORDER BY `roles`.`id` ASC LIMIT 1
(0.2ms) BEGIN
SQL (0.6ms) INSERT INTO `roles` (`name`, `resource_type`, `resource_id`, `created_at`, `updated_at`) VALUES ('pi', 'ProjectRequest', 319, '2018-06-19 11:40:13', '2018-06-19 11:40:13')
(54.3ms) ROLLBACK
NoMethodError: undefined method `primary_key' for User::ActiveRecord_Associations_CollectionProxy:Class
I don't really now whats the problem, did I wrongly specified something?
*HABTM (has and belongs to many) not HMABT (has many and belongs to)
any follow up on this? I am having the same problem with a has_many through... relation
@beeberino
My current configuration looks like this
class User < ApplicationRecord
rolify strict: true
has_many :users_roles
has_many :roles, through: :users_roles, dependent: :destroy
has_associated_audits
class Role < ApplicationRecord
# has_and_belongs_to_many :users, join_table: :users_roles
has_many :users_roles
has_many :users, through: :users_roles, dependent: :destroy
I did not solve HABTM
Just ran into this as well. It looks like this is a limitation of audited: https://github.com/collectiveidea/audited/issues/316
has_associated_audits
is meant to mirror audited associated_with:
, but the latter assumes it is dealing with a belongs_to
relationship. So when it tries to write the audit here, it tries to save what it thinks is the belongs_to
association to the polymorphic associated
on the Audit
model, and that returns an array rather than a record, which ActiveRecord is rightfully very upset about.
I found a solution for my use case, so far, at least.
Thankfully, rolify
passes the after_add
and after_remove
callbacks to the HABTM. See https://github.com/RolifyCommunity/rolify/blob/0c883f4173f409766338b9c6dfc64b0fc8ec8a52/lib/rolify.rb#L28-L30.
Using the HABTM callbacks, which are passed to has_many
, you can insert a new audit record manually.
For example:
class User < ApplicationRecord
rolify after_add: :audit_add_role, after_remove: :audit_remove_role
audited
private
def audit_add_role(role)
audits.create! action: :add_role, associated: role
end
def audit_remove_role(role)
audits.create! action: :remove_role, associated: role
end
end
The solution is not perfect and has its limitations. For instance, calling undo
on the audit record will raise an exception. A workaround might be monkey patching the Audited::Audit
model to handle the undoing of HABTM records.
You could do something like this (this is untested and not perfect, just writing it out here for an example):
module AuditHasAndBelongsToManyFix
def undo
case action
when "add"
# You'll need to find a way to get the relation name.
# One idea is to store it in the `audited_changes`; you'll need to make sure to do this in the HABTM callbacks
public_send(audited_changes['relation']).delete associated
when "remove"
public_send(audited_changes['relation']).push associated
else
super
end
end
end
ActiveSupport.on_load(:active_record) { Audited::Audit.prepend AuditHasAndBelongsToManyFix }
I ended up getting around the problem with has_and_belongs_to_many
by using the callbacks to create manual auditions for my user model.
# Inside my user.rb model:
has_and_belongs_to_many :other_model, after_add: :audit_add, before_remove: :audit_remove
def audit_add(other_model)
audits.create!(
associated_id: other_model.id,
associated_type: other_model.class,
audited_changes: "Included association with #{other_model.class} ID #{other_model.id}."
)
end
def audit_remove(other_model)
audits.create!(
associated_id: other_model.id,
associated_type: other_model.class,
audited_changes: "Removed association with #{other_model.class} ID #{other_model.id}."
)
end
In my case, a legacy application that we didn't want to make too many changes just to have a history of relationship additions or removals, this worked.
I hope it can help some of the folks here.