collectiveidea / audited

Audited (formerly acts_as_audited) is an ORM extension that logs all changes to your Rails models.
MIT License
3.4k stars 691 forks source link

Associated audits with HABTM relation #458

Open Kani999 opened 6 years ago

Kani999 commented 6 years ago

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?

SampsonCrowley commented 6 years ago

*HABTM (has and belongs to many) not HMABT (has many and belongs to)

beeberino commented 5 years ago

any follow up on this? I am having the same problem with a has_many through... relation

Kani999 commented 5 years ago

@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

thec0keman commented 3 years ago

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.

jefawks3 commented 2 years ago

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 } 
andrerohde commented 1 year ago

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.