diff --git a/decidim-accountability/app/models/decidim/accountability/result.rb b/decidim-accountability/app/models/decidim/accountability/result.rb index 2bf46097ec04e..ec5506398f719 100644 --- a/decidim-accountability/app/models/decidim/accountability/result.rb +++ b/decidim-accountability/app/models/decidim/accountability/result.rb @@ -11,11 +11,10 @@ class Result < Accountability::ApplicationRecord include Decidim::HasCategory include Decidim::HasReference include Decidim::Comments::Commentable + include Decidim::Traceable feature_manifest_name "accountability" - has_paper_trail - has_many :children, foreign_key: "parent_id", class_name: "Decidim::Accountability::Result", inverse_of: :parent, dependent: :destroy belongs_to :parent, foreign_key: "parent_id", class_name: "Decidim::Accountability::Result", inverse_of: :children, optional: true, counter_cache: :children_count diff --git a/decidim-core/app/helpers/decidim/traceability_helper.rb b/decidim-core/app/helpers/decidim/traceability_helper.rb index 5c939883ec4ab..383cbce2eb2b1 100644 --- a/decidim-core/app/helpers/decidim/traceability_helper.rb +++ b/decidim-core/app/helpers/decidim/traceability_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Decidim - # A Helper to render and link to resources. + # A Helper to find and render the author of a version. module TraceabilityHelper # Renders the avatar and author name of the author of the last version of the given # resource. diff --git a/decidim-core/app/services/decidim/traceability.rb b/decidim-core/app/services/decidim/traceability.rb new file mode 100644 index 0000000000000..79797d4f42688 --- /dev/null +++ b/decidim-core/app/services/decidim/traceability.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Decidim + # This class wraps the logic to trace resource changes and their authorship. + # It is expected to be used with classes implementing the `Decidim::Traceable` + # concern. Version authors can be retrieved using the methods in + # `Decidim::TraceabilityHelper`. + # + # Examples: + # + # # consider MyResource implements Decidim::Traceable + # resource = Decidim::Traceability.new.create!(MyResource, author, params) + # resource.versions.count # => 1 + # resource.versions.last.whodunnit # => author.to_gid.to_s + # resource.versions.last.event # => "create" + # resource = Decidim::Traceability.new.update!(resource, author, params) + # resource.versions.count # => 2 + # resource.versions.last.event # => "update" + # + # This class uses the `paper_trail` gem internally, so refer to its documentation + # for further info on how to interact with versions. + class Traceability + # Calls the `create` method to the given class and sets the author of the version. + # + # klass - An ActiveRecord class that implements `Decidim::Traceable` + # author - An object that implements `to_gid` or a String + # params - a Hash + # + # Returns an instance of `klass`. + def create(klass, author, params) + PaperTrail.whodunnit(gid(author)) do + klass.create(params) + end + end + + # Calls the `create!` method to the given class and sets the author of the version. + # + # klass - An ActiveRecord class that implements `Decidim::Traceable` + # author - An object that implements `to_gid` or a String + # params - a Hash + # + # Returns an instance of `klass`. + def create!(klass, author, params) + PaperTrail.whodunnit(gid(author)) do + klass.create!(params) + end + end + + # Updates the `resource` with `update_attributes!` and sets the author of the version. + # + # resource - An ActiveRecord instance that implements `Decidim::Traceable` + # author - An object that implements `to_gid` or a String + # params - a Hash + # + # Returns the updated `resource`. + def update!(resource, author, params) + PaperTrail.whodunnit(gid(author)) do + resource.update_attributes!(params) + resource + end + end + + private + + # Calculates the GlobalID of the version author. If the object does not respond to + # `to_gid`, then it returns the object itself. + def gid(author) + return if author.blank? + return author.to_gid if author.respond_to?(:to_gid) + author + end + end +end diff --git a/decidim-core/lib/decidim/core.rb b/decidim-core/lib/decidim/core.rb index 536d97de857e1..d61294ef94219 100644 --- a/decidim-core/lib/decidim/core.rb +++ b/decidim-core/lib/decidim/core.rb @@ -15,6 +15,7 @@ module Decidim autoload :ParticipatorySpaceManifest, "decidim/participatory_space_manifest" autoload :ResourceManifest, "decidim/resource_manifest" autoload :Resourceable, "decidim/resourceable" + autoload :Traceable, "decidim/traceable" autoload :Reportable, "decidim/reportable" autoload :Authorable, "decidim/authorable" autoload :Participable, "decidim/participable" @@ -255,4 +256,9 @@ def self.menu(name, &block) def self.view_hooks @view_hooks ||= ViewHooks.new end + + # Public: Stores an instance of Traceability + def self.traceability + @traceability ||= Traceability.new + end end diff --git a/decidim-core/lib/decidim/traceable.rb b/decidim-core/lib/decidim/traceable.rb new file mode 100644 index 0000000000000..8ec7167edb0bc --- /dev/null +++ b/decidim-core/lib/decidim/traceable.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + # A concern that adds traceabilty capability to the given model. Including this + # allows you the keep track of changes in the model attributes and changes authorship. + # + # Example: + # + # class MyModel < ApplicationRecord + # include Decidim::Traceable + # end + module Traceable + extend ActiveSupport::Concern + + included do + has_paper_trail + end + end +end diff --git a/decidim-core/spec/services/decidim/traceability_spec.rb b/decidim-core/spec/services/decidim/traceability_spec.rb new file mode 100644 index 0000000000000..acb3316b9b2b8 --- /dev/null +++ b/decidim-core/spec/services/decidim/traceability_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Decidim::Traceability, versioning: true do + let!(:user) { create :user } + let(:klass) { Decidim::DummyResources::DummyResource } + let(:params) { attributes_for(:dummy_resource) } + subject { described_class.new } + + describe "create" do + it "calls `create` to the class" do + expect(klass).to receive(:create).with(params) + subject.create(klass, user, params) + end + + it "generates a new version for the resource" do + resource = subject.create(klass, user, params) + expect(resource.versions.count).to eq 1 + expect(resource.versions.last.event).to eq "create" + end + + it "sets the author of the version to the user" do + resource = subject.create(klass, user, params) + expect(resource.versions.last.whodunnit).to eq user.to_gid.to_s + end + end + + describe "create!" do + it "calls `create!` to the class" do + expect(klass).to receive(:create!).with(params) + subject.create!(klass, user, params) + end + + it "generates a new version for the resource" do + resource = subject.create!(klass, user, params) + expect(resource.versions.count).to eq 1 + expect(resource.versions.last.event).to eq "create" + end + + it "sets the author of the version to the user" do + resource = subject.create!(klass, user, params) + expect(resource.versions.last.whodunnit).to eq user.to_gid.to_s + end + end + + describe "update!" do + let(:dummy_resource) { create :dummy_resource } + + it "calls `update_attributes!` to the resource" do + expect(dummy_resource).to receive(:update_attributes!).with(params) + subject.update!(dummy_resource, user, params) + end + + it "generates a new version for the resource" do + resource = subject.update!(dummy_resource, user, params) + expect(resource.versions.count).to eq 2 + expect(resource.versions.last.event).to eq "update" + end + + it "sets the author of the version to the user" do + resource = subject.update!(dummy_resource, user, params) + expect(resource.versions.last.whodunnit).to eq user.to_gid.to_s + end + end +end diff --git a/decidim-dev/lib/decidim/dev/test/rspec_support/feature.rb b/decidim-dev/lib/decidim/dev/test/rspec_support/feature.rb index 6142661bcd489..ef739f4e645e8 100644 --- a/decidim-dev/lib/decidim/dev/test/rspec_support/feature.rb +++ b/decidim-dev/lib/decidim/dev/test/rspec_support/feature.rb @@ -41,6 +41,7 @@ class DummyResource < ApplicationRecord include HasScope include Decidim::Comments::Commentable include Followable + include Traceable feature_manifest_name "dummy"