diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 39dc2c7ae..93327302c 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -288,7 +288,7 @@ class Dossier < ApplicationRecord def passer_automatiquement_en_instruction! en_instruction! - log_dossier_operation(nil, :passer_en_instruction, automatic_operation: true) + log_automatic_dossier_operation(:passer_en_instruction) end def repasser_en_construction!(gestionnaire) @@ -311,7 +311,7 @@ class Dossier < ApplicationRecord end NotificationMailer.send_closed_notification(self).deliver_later - log_dossier_operation(gestionnaire, :accepter) + log_dossier_operation(gestionnaire, :accepter, self) end def accepter_automatiquement! @@ -324,14 +324,14 @@ class Dossier < ApplicationRecord end NotificationMailer.send_closed_notification(self).deliver_later - log_dossier_operation(nil, :accepter, automatic_operation: true) + log_automatic_dossier_operation(:accepter, self) end def hide!(administration) update(hidden_at: Time.zone.now) - log_administration_dossier_operation(administration, :supprimer) DeletedDossier.create_from_dossier(self) + log_dossier_operation(administration, :supprimer, self) end def refuser!(gestionnaire, motivation, justificatif = nil) @@ -343,7 +343,7 @@ class Dossier < ApplicationRecord refuse! NotificationMailer.send_refused_notification(self).deliver_later - log_dossier_operation(gestionnaire, :refuser) + log_dossier_operation(gestionnaire, :refuser, self) end def classer_sans_suite!(gestionnaire, motivation, justificatif = nil) @@ -355,7 +355,7 @@ class Dossier < ApplicationRecord sans_suite! NotificationMailer.send_without_continuation_notification(self).deliver_later - log_dossier_operation(gestionnaire, :classer_sans_suite) + log_dossier_operation(gestionnaire, :classer_sans_suite, self) end def check_mandatory_champs @@ -368,18 +368,21 @@ class Dossier < ApplicationRecord private - def log_dossier_operation(gestionnaire, operation, automatic_operation: false) - dossier_operation_logs.create( - gestionnaire: gestionnaire, + def log_dossier_operation(author, operation, subject = nil) + DossierOperationLog.create_and_serialize( + dossier: self, operation: DossierOperationLog.operations.fetch(operation), - automatic_operation: automatic_operation + author: author, + subject: subject ) end - def log_administration_dossier_operation(administration, operation) - dossier_operation_logs.create( - administration: administration, - operation: DossierOperationLog.operations.fetch(operation) + def log_automatic_dossier_operation(operation, subject = nil) + DossierOperationLog.create_and_serialize( + dossier: self, + operation: DossierOperationLog.operations.fetch(operation), + automatic_operation: true, + subject: subject ) end diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index 6dced86b6..8cda68fde 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -9,6 +9,57 @@ class DossierOperationLog < ApplicationRecord } belongs_to :dossier - belongs_to :gestionnaire - belongs_to :administration + has_one_attached :serialized + + def self.create_and_serialize(params) + dossier = params.fetch(:dossier) + + operation_log = new(operation: params.fetch(:operation), + dossier_id: dossier.id, + keep_until: dossier.procedure.keep_until, + executed_at: Time.zone.now, + automatic_operation: !!params[:automatic_operation]) + + serialized = { + operation: operation_log.operation, + dossier_id: operation_log.dossier_id, + author: self.serialize_author(params[:author]), + subject: self.serialize_subject(params[:subject]), + automatic_operation: operation_log.automatic_operation?, + executed_at: operation_log.executed_at.iso8601 + }.compact.to_json + + operation_log.digest = Digest::SHA256.hexdigest(serialized) + + operation_log.serialized.attach( + io: StringIO.new(serialized), + filename: "operation-#{operation_log.digest}.json", + content_type: 'application/json', + # we don't want to run virus scanner on this file + metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } + ) + + operation_log.save! + end + + def self.serialize_author(author) + if author.nil? + nil + else + OperationAuthorSerializer.new(author).as_json + end + end + + def self.serialize_subject(subject) + if subject.nil? + nil + elsif !Flipflop.operation_log_serialize_subject? + { id: subject.id } + else + case subject + when Dossier + DossierSerializer.new(subject).as_json + end + end + end end diff --git a/app/serializers/operation_author_serializer.rb b/app/serializers/operation_author_serializer.rb new file mode 100644 index 000000000..0856a1e90 --- /dev/null +++ b/app/serializers/operation_author_serializer.rb @@ -0,0 +1,18 @@ +class OperationAuthorSerializer < ActiveModel::Serializer + attributes :id, :email + + def id + case object + when User + "Usager##{object.id}" + when Gestionnaire + "Instructeur##{object.id}" + when Administrateur + "Administrateur##{object.id}" + when Administration + "Manager##{object.id}" + else + nil + end + end +end diff --git a/config/features.rb b/config/features.rb index f42abcf32..13f6d592a 100644 --- a/config/features.rb +++ b/config/features.rb @@ -17,6 +17,8 @@ Flipflop.configure do feature :enable_email_login_token feature :new_champs_editor + feature :operation_log_serialize_subject + group :production do feature :remote_storage, default: ENV['FOG_ENABLED'] == 'enabled' diff --git a/spec/jobs/auto_archive_procedure_job_spec.rb b/spec/jobs/auto_archive_procedure_job_spec.rb index 2103f8a8a..8e0225357 100644 --- a/spec/jobs/auto_archive_procedure_job_spec.rb +++ b/spec/jobs/auto_archive_procedure_job_spec.rb @@ -27,6 +27,7 @@ RSpec.describe AutoArchiveProcedureJob, type: :job do let!(:dossier7) { create(:dossier, procedure: procedure_hier, state: Dossier.states.fetch(:refuse), archived: false) } let!(:dossier8) { create(:dossier, procedure: procedure_hier, state: Dossier.states.fetch(:sans_suite), archived: false) } let!(:dossier9) { create(:dossier, procedure: procedure_aujourdhui, state: Dossier.states.fetch(:en_construction), archived: false) } + let(:last_operation) { dossier2.dossier_operation_logs.last } before do subject @@ -40,7 +41,8 @@ RSpec.describe AutoArchiveProcedureJob, type: :job do it { expect(dossier1.state).to eq Dossier.states.fetch(:brouillon) expect(dossier2.state).to eq Dossier.states.fetch(:en_instruction) - expect(dossier2.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'passer_en_instruction', true]]) + expect(last_operation.operation).to eq('passer_en_instruction') + expect(last_operation.automatic_operation?).to be_truthy expect(dossier3.state).to eq Dossier.states.fetch(:en_instruction) expect(dossier4.state).to eq Dossier.states.fetch(:en_instruction) expect(dossier5.state).to eq Dossier.states.fetch(:en_instruction) diff --git a/spec/jobs/auto_receive_dossiers_for_procedure_job_spec.rb b/spec/jobs/auto_receive_dossiers_for_procedure_job_spec.rb index a2ab0a799..517a6dde9 100644 --- a/spec/jobs/auto_receive_dossiers_for_procedure_job_spec.rb +++ b/spec/jobs/auto_receive_dossiers_for_procedure_job_spec.rb @@ -31,11 +31,13 @@ RSpec.describe AutoReceiveDossiersForProcedureJob, type: :job do context "with some dossiers" do context "en_construction" do let(:state) { Dossier.states.fetch(:en_instruction) } + let(:last_operation) { nouveau_dossier1.dossier_operation_logs.last } it { expect(nouveau_dossier1.en_instruction?).to be true expect(nouveau_dossier1.en_instruction_at).to eq(date) - expect(nouveau_dossier1.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'passer_en_instruction', true]]) + expect(last_operation.operation).to eq('passer_en_instruction') + expect(last_operation.automatic_operation?).to be_truthy expect(nouveau_dossier2.en_instruction?).to be true expect(nouveau_dossier2.en_instruction_at).to eq(date) @@ -50,13 +52,15 @@ RSpec.describe AutoReceiveDossiersForProcedureJob, type: :job do context "accepte" do let(:state) { Dossier.states.fetch(:accepte) } + let(:last_operation) { nouveau_dossier1.dossier_operation_logs.last } it { expect(nouveau_dossier1.accepte?).to be true expect(nouveau_dossier1.en_instruction_at).to eq(date) expect(nouveau_dossier1.processed_at).to eq(date) expect(nouveau_dossier1.attestation).to be_present - expect(nouveau_dossier1.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'accepter', true]]) + expect(last_operation.operation).to eq('accepter') + expect(last_operation.automatic_operation?).to be_truthy expect(nouveau_dossier2.accepte?).to be true expect(nouveau_dossier2.en_instruction_at).to eq(date) diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index ea85160af..a39b96117 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -764,6 +764,8 @@ describe Dossier do describe '#accepter!' do let(:dossier) { create(:dossier) } + let(:last_operation) { dossier.dossier_operation_logs.last } + let(:operation_serialized) { JSON.parse(last_operation.serialized.download) } let!(:gestionnaire) { create(:gestionnaire) } let!(:now) { Time.zone.parse('01/01/2100') } let(:attestation) { Attestation.new } @@ -783,13 +785,18 @@ describe Dossier do it { expect(dossier.en_instruction_at).to eq(now) } it { expect(dossier.processed_at).to eq(now) } it { expect(dossier.state).to eq('accepte') } - it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[gestionnaire.id, 'accepter', false]]) } + it { expect(last_operation.operation).to eq('accepter') } + it { expect(last_operation.automatic_operation?).to be_falsey } + it { expect(operation_serialized['operation']).to eq('accepter') } + it { expect(operation_serialized['dossier_id']).to eq(dossier.id) } + it { expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601) } it { expect(NotificationMailer).to have_received(:send_closed_notification).with(dossier) } it { expect(dossier.attestation).to eq(attestation) } end describe '#accepter_automatiquement!' do let(:dossier) { create(:dossier) } + let(:last_operation) { dossier.dossier_operation_logs.last } let!(:now) { Time.zone.parse('01/01/2100') } let(:attestation) { Attestation.new } @@ -808,30 +815,43 @@ describe Dossier do it { expect(dossier.en_instruction_at).to eq(now) } it { expect(dossier.processed_at).to eq(now) } it { expect(dossier.state).to eq('accepte') } - it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'accepter', true]]) } + it { expect(last_operation.operation).to eq('accepter') } + it { expect(last_operation.automatic_operation?).to be_truthy } it { expect(NotificationMailer).to have_received(:send_closed_notification).with(dossier) } it { expect(dossier.attestation).to eq(attestation) } end describe '#passer_en_instruction!' do let(:dossier) { create(:dossier) } + let(:last_operation) { dossier.dossier_operation_logs.last } + let(:operation_serialized) { JSON.parse(last_operation.serialized.download) } let(:gestionnaire) { create(:gestionnaire) } before { dossier.passer_en_instruction!(gestionnaire) } it { expect(dossier.state).to eq('en_instruction') } it { expect(dossier.followers_gestionnaires).to include(gestionnaire) } - it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation)).to match([[gestionnaire.id, 'passer_en_instruction']]) } + it { expect(last_operation.operation).to eq('passer_en_instruction') } + it { expect(last_operation.automatic_operation?).to be_falsey } + it { expect(operation_serialized['operation']).to eq('passer_en_instruction') } + it { expect(operation_serialized['dossier_id']).to eq(dossier.id) } + it { expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601) } end describe '#passer_automatiquement_en_instruction!' do let(:dossier) { create(:dossier) } + let(:last_operation) { dossier.dossier_operation_logs.last } + let(:operation_serialized) { JSON.parse(last_operation.serialized.download) } let(:gestionnaire) { create(:gestionnaire) } before { dossier.passer_automatiquement_en_instruction! } it { expect(dossier.followers_gestionnaires).not_to include(gestionnaire) } - it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'passer_en_instruction', true]]) } + it { expect(last_operation.operation).to eq('passer_en_instruction') } + it { expect(last_operation.automatic_operation?).to be_truthy } + it { expect(operation_serialized['operation']).to eq('passer_en_instruction') } + it { expect(operation_serialized['dossier_id']).to eq(dossier.id) } + it { expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601) } end describe "#check_mandatory_champs" do @@ -934,6 +954,6 @@ describe Dossier do it { expect(dossier.hidden_at).to eq(Time.zone.now) } it { expect(last_operation.operation).to eq('supprimer') } - it { expect(last_operation.administration).to eq(administration) } + it { expect(last_operation.automatic_operation?).to be_falsey } end end