Merge pull request #3782 from tchak/log-more-dossier-operations

Track dossier changes
This commit is contained in:
Nicolas Bouilleaud 2019-05-14 15:16:30 +02:00 committed by GitHub
commit 2632ccef9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 186 additions and 27 deletions

View file

@ -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?

View file

@ -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

View file

@ -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
@ -366,20 +366,33 @@ 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
def demander_un_avis!(avis)
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end
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

View file

@ -5,10 +5,76 @@ class DossierOperationLog < ApplicationRecord
accepter: 'accepter',
refuser: 'refuser',
classer_sans_suite: 'classer_sans_suite',
supprimer: 'supprimer'
supprimer: 'supprimer',
modifier_annotation: 'modifier_annotation',
demander_un_avis: 'demander_un_avis'
}
belongs_to :dossier
belongs_to :gestionnaire
belongs_to :administration
has_one_attached :serialized
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: 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
when Champ
ChampSerializer.new(subject).as_json
when Avis
AvisSerializer.new(subject).as_json
end
end
end
end

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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|

View file

@ -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)

View file

@ -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)

View file

@ -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