collectiveidea / audited

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

Deletion of joining records not being audited on Collection Associations #546

Open abhchand opened 4 years ago

abhchand commented 4 years ago

Hey audit gem maintainers 👋

Found a small issue that I'd love your feedback on. Also, really appreciate all the time the maintainers of this gem put into this great open source project.

Model Structure

Say I have the following model structure -

The rails models look like this:

class Employee < ApplicationRecord
  audited

  has_many :employee_projects, dependent: :destroy
  has_many :projects, through: :employee_projects
  accepts_nested_attributes_for :projects, allow_destroy: true
end

class Project < ApplicationRecord
  audited

  has_many :employee_projects, dependent: :destroy
  has_many :employees, through: :employee_projects
  accepts_nested_attributes_for :employees, allow_destroy: true
end

class EmployeeProject < ApplicationRecord
  audited

  belongs_to :employee
  belongs_to :project

  validates :employee, presence: true
  validates :project, presence: true
end

The Issue

Rails provides collection association helpers on models that use the has_many :through association.

So in the above example rails automatically provides the following -

These helpers are absolute - they automatically add and remove any necessary joining table records based on the array you provide them. So @employe.project_ids = [1, 2] would ensure that only projects 1, and 2 are joined to this @employee record. If any other records exist, they will be destroyed when the model is saved. (EDIT: It actually calls save itself automatically, so changes take effect immediately)

The issue is that the Audit gem correctly audits any newly created joining records using the above helpers, but it does not correctly audit any destroyed joining records.

Example

Initialize models and note that there are no audit records

> @employee = Employee.first
> @project = Project.first

> Audited::Audit.count   # => 0

Associate @employee and @project using the project_ids= collection association helper.

This correctly creates an audit record for the new EmployeeProject joining record

> @employee.project_ids = [@project.id]  # => [15]
> @employee.save                         # => true

> Audited::Audit.count                   # => 1
> Audited::Audit.first.auditable_type    # => "EmployeeProject"

Now disassociate @employee and @project which will delete the EmployeeProject joining record.

This should create another Audit record for the deletion of the joining record... but it doesn't

> @employee.project_ids = []             # => []
> @employee.save                         # => true

> Audited::Audit.count                   # => 1
chase439 commented 3 years ago

I had the same problem and found a solution.

For you, adding "dependent: :destroy" to your "has_many through" statement should make it work:

class Employee < ApplicationRecord
    has_many :projects, through: :employee_projects, dependent: :destroy
end

Note that destroying the the employee does not destroy the projects. This is more of a Rails oddity.

Also, it's a bit weird you have audited on all three models. I would suggest changing it to:

class Employee < ApplicationRecord
  audited  ## Assuming Employee is the main model where you will compile "own_and_associated_audits" data
  has_associated_audits

  has_many :employee_projects, dependent: :destroy
  has_many :projects, through: :employee_projects, dependent: :destroy  ## <== CHANGED
  accepts_nested_attributes_for :projects, allow_destroy: true
end

class Project < ApplicationRecord
   ## <== CHANGED, removed audited unless you need it from this model

  has_many :employee_projects, dependent: :destroy
  has_many :employees, through: :employee_projects
  accepts_nested_attributes_for :employees, allow_destroy: true
end

class EmployeeProject < ApplicationRecord
  audited associated_with: :employee  ## <== CHANGED

  belongs_to :employee
  belongs_to :project

  validates :employee, presence: true
  validates :project, presence: true
end
mscdit commented 5 months ago

Hi @chase439 and @abhchand ... adding dependent: :destroy didn't solve this for me. I can see the SQL log entry for deleting the entry in the joining table but no "destroy" audit is created in that case.

Any further hints?

THX, mscdit

kylebragger commented 1 month ago

Came here to open the same issue - I've got everything configured with has_associated_audits and associated_with but deletions aren't logged anywhere. Using v5.7.0