From 3c4380251da19dff08047e76a3e6db091b5d71d3 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 2 May 2019 16:18:10 +0200 Subject: [PATCH 1/5] Add digest and timestamps to operation_logs --- ...add_digest_and_timestamps_to_dossier_operation_logs.rb | 8 ++++++++ db/schema.rb | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20190327102360_add_digest_and_timestamps_to_dossier_operation_logs.rb diff --git a/db/migrate/20190327102360_add_digest_and_timestamps_to_dossier_operation_logs.rb b/db/migrate/20190327102360_add_digest_and_timestamps_to_dossier_operation_logs.rb new file mode 100644 index 000000000..235e78f78 --- /dev/null +++ b/db/migrate/20190327102360_add_digest_and_timestamps_to_dossier_operation_logs.rb @@ -0,0 +1,8 @@ +class AddDigestAndTimestampsToDossierOperationLogs < ActiveRecord::Migration[5.2] + def change + add_column :dossier_operation_logs, :keep_until, :datetime + add_column :dossier_operation_logs, :executed_at, :datetime + add_column :dossier_operation_logs, :digest, :text + add_index :dossier_operation_logs, :keep_until + end +end diff --git a/db/schema.rb b/db/schema.rb index aeceeaf2f..c0f9b1a41 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_03_27_102357) do +ActiveRecord::Schema.define(version: 2019_03_27_102360) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -221,9 +221,13 @@ ActiveRecord::Schema.define(version: 2019_03_27_102357) do t.datetime "updated_at", null: false t.boolean "automatic_operation", default: false, null: false t.bigint "administration_id" + t.datetime "keep_until" + t.datetime "executed_at" + t.text "digest" t.index ["administration_id"], name: "index_dossier_operation_logs_on_administration_id" t.index ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id" t.index ["gestionnaire_id"], name: "index_dossier_operation_logs_on_gestionnaire_id" + t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until" end create_table "dossiers", id: :serial, force: :cascade do |t| From dba8d65137534a29c083c0a01be4ee8dd8044094 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 2 May 2019 16:22:16 +0200 Subject: [PATCH 2/5] Track dossier operations with author and subject --- app/models/dossier.rb | 31 ++++++----- app/models/dossier_operation_log.rb | 55 ++++++++++++++++++- .../operation_author_serializer.rb | 18 ++++++ config/features.rb | 2 + spec/jobs/auto_archive_procedure_job_spec.rb | 4 +- ...receive_dossiers_for_procedure_job_spec.rb | 8 ++- spec/models/dossier_spec.rb | 30 ++++++++-- 7 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 app/serializers/operation_author_serializer.rb 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 From 9c472e6524379706a086d0199d8531dfeec624fa Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 2 May 2019 16:23:47 +0200 Subject: [PATCH 3/5] Track dossier private annotations modifications --- app/controllers/gestionnaires/dossiers_controller.rb | 2 +- app/models/dossier.rb | 5 +++++ app/models/dossier_operation_log.rb | 5 ++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/controllers/gestionnaires/dossiers_controller.rb b/app/controllers/gestionnaires/dossiers_controller.rb index a96616bfe..d7f7d06fa 100644 --- a/app/controllers/gestionnaires/dossiers_controller.rb +++ b/app/controllers/gestionnaires/dossiers_controller.rb @@ -136,8 +136,8 @@ module Gestionnaires def update_annotations dossier = current_gestionnaire.dossiers.includes(champs_private: :type_de_champ).find(params[:dossier_id]) - # FIXME: add attachements validation, cf. Champ#piece_justificative_file_errors dossier.update(champs_private_params) + dossier.modifier_annotations!(current_gestionnaire) redirect_to annotations_privees_gestionnaire_dossier_path(procedure, dossier) end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 93327302c..4024f11e4 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -366,6 +366,11 @@ class Dossier < ApplicationRecord end end + def modifier_annotations!(gestionnaire) + champs_private.select(&:value_previously_changed?).each do |champ| + log_dossier_operation(gestionnaire, :modifier_annotation, champ) + end + end private def log_dossier_operation(author, operation, subject = nil) diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index 8cda68fde..2ce152d04 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -5,7 +5,8 @@ class DossierOperationLog < ApplicationRecord accepter: 'accepter', refuser: 'refuser', classer_sans_suite: 'classer_sans_suite', - supprimer: 'supprimer' + supprimer: 'supprimer', + modifier_annotation: 'modifier_annotation', } belongs_to :dossier @@ -59,6 +60,8 @@ class DossierOperationLog < ApplicationRecord case subject when Dossier DossierSerializer.new(subject).as_json + when Champ + ChampSerializer.new(subject).as_json end end end From b8bf662c6d9f9aafc5fc71db72b77d0d30f0a27a Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 2 May 2019 16:24:24 +0200 Subject: [PATCH 4/5] Track dossier demander un avis --- .../concerns/create_avis_concern.rb | 3 +++ app/models/dossier.rb | 5 +++++ app/models/dossier_operation_log.rb | 3 +++ app/serializers/avis_serializer.rb | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 app/serializers/avis_serializer.rb diff --git a/app/controllers/concerns/create_avis_concern.rb b/app/controllers/concerns/create_avis_concern.rb index 87143a3ce..7f9514509 100644 --- a/app/controllers/concerns/create_avis_concern.rb +++ b/app/controllers/concerns/create_avis_concern.rb @@ -29,6 +29,9 @@ module CreateAvisConcern if persisted.any? sent_emails_addresses = persisted.map(&:email_to_display).join(", ") flash.notice = "Une demande d'avis a été envoyée à #{sent_emails_addresses}" + persisted.each do |avis| + dossier.demander_un_avis!(avis) + end end if failed.any? diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 4024f11e4..abcfd983f 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -371,6 +371,11 @@ class Dossier < ApplicationRecord log_dossier_operation(gestionnaire, :modifier_annotation, champ) end end + + def demander_un_avis!(avis) + log_dossier_operation(avis.claimant, :demander_un_avis, avis) + end + private def log_dossier_operation(author, operation, subject = nil) diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index 2ce152d04..3d251cccf 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -7,6 +7,7 @@ class DossierOperationLog < ApplicationRecord classer_sans_suite: 'classer_sans_suite', supprimer: 'supprimer', modifier_annotation: 'modifier_annotation', + demander_un_avis: 'demander_un_avis' } belongs_to :dossier @@ -62,6 +63,8 @@ class DossierOperationLog < ApplicationRecord DossierSerializer.new(subject).as_json when Champ ChampSerializer.new(subject).as_json + when Avis + AvisSerializer.new(subject).as_json end end end diff --git a/app/serializers/avis_serializer.rb b/app/serializers/avis_serializer.rb new file mode 100644 index 000000000..7811c1152 --- /dev/null +++ b/app/serializers/avis_serializer.rb @@ -0,0 +1,19 @@ +class AvisSerializer < ActiveModel::Serializer + attributes :email, + :answer, + :introduction, + :created_at, + :answered_at + + def email + object.email_to_display + end + + def created_at + object.created_at&.in_time_zone('UTC') + end + + def answered_at + object.updated_at&.in_time_zone('UTC') + end +end From 9ce1f0d0a6bf8a640e721b26e0fbf03efbc03b8d Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 2 May 2019 17:02:47 +0200 Subject: [PATCH 5/5] Set keep_until on operation_log if available on procedure --- app/models/dossier_operation_log.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index 3d251cccf..e517dcb36 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -16,9 +16,18 @@ class DossierOperationLog < ApplicationRecord def self.create_and_serialize(params) dossier = params.fetch(:dossier) + duree_conservation_dossiers = dossier.procedure.duree_conservation_dossiers_dans_ds + keep_until = if duree_conservation_dossiers.present? + if dossier.en_instruction_at + dossier.en_instruction_at + duree_conservation_dossiers.months + else + dossier.created_at + duree_conservation_dossiers.months + end + end + operation_log = new(operation: params.fetch(:operation), dossier_id: dossier.id, - keep_until: dossier.procedure.keep_until, + keep_until: keep_until, executed_at: Time.zone.now, automatic_operation: !!params[:automatic_operation])