Merge pull request #6634 from betagouv/main

2021-11-12-01
This commit is contained in:
Paul Chavard 2021-11-12 11:08:16 +01:00 committed by GitHub
commit 9500bbf7c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 136 additions and 41 deletions

View file

@ -83,6 +83,18 @@ class Dossier < ApplicationRecord
has_many :avis, inverse_of: :dossier, dependent: :destroy has_many :avis, inverse_of: :dossier, dependent: :destroy
has_many :experts, through: :avis has_many :experts, through: :avis
has_many :traitements, -> { order(:processed_at) }, inverse_of: :dossier, dependent: :destroy do has_many :traitements, -> { order(:processed_at) }, inverse_of: :dossier, dependent: :destroy do
def passer_en_construction(processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:en_construction),
process_expired: false,
processed_at: processed_at)
end
def passer_en_instruction(processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:en_instruction),
process_expired: false,
processed_at: processed_at)
end
def accepter_automatiquement(processed_at: Time.zone.now) def accepter_automatiquement(processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:accepte), build(state: Dossier.states.fetch(:accepte),
process_expired: proxy_association.owner.procedure.feature_enabled?(:procedure_process_expired_dossiers_termine), process_expired: proxy_association.owner.procedure.feature_enabled?(:procedure_process_expired_dossiers_termine),
@ -113,6 +125,7 @@ class Dossier < ApplicationRecord
processed_at: processed_at) processed_at: processed_at)
end end
end end
has_one :traitement, -> { order(processed_at: :desc) }, inverse_of: false
has_many :dossier_operation_logs, -> { order(:created_at) }, inverse_of: :dossier has_many :dossier_operation_logs, -> { order(:created_at) }, inverse_of: :dossier
@ -221,7 +234,7 @@ class Dossier < ApplicationRecord
:user, :user,
:individual, :individual,
:followers_instructeurs, :followers_instructeurs,
:traitements, :traitement,
:groupe_instructeur, :groupe_instructeur,
procedure: [ procedure: [
:groupe_instructeurs, :groupe_instructeurs,
@ -258,7 +271,7 @@ class Dossier < ApplicationRecord
justificatif_motivation_attachment: :blob, justificatif_motivation_attachment: :blob,
attestation: [], attestation: [],
avis: { piece_justificative_file_attachment: :blob }, avis: { piece_justificative_file_attachment: :blob },
traitements: [], traitement: [],
etablissement: [], etablissement: [],
individual: [], individual: [],
user: []) user: [])
@ -342,7 +355,7 @@ class Dossier < ApplicationRecord
.where.not(user: users_who_submitted) .where.not(user: users_who_submitted)
end end
scope :for_api_v2, -> { includes(procedure: [:administrateurs, :attestation_template], etablissement: [], individual: [], traitements: []) } scope :for_api_v2, -> { includes(procedure: [:administrateurs, :attestation_template], etablissement: [], individual: [], traitement: []) }
scope :with_notifications, -> do scope :with_notifications, -> do
joins(:follows) joins(:follows)
@ -423,12 +436,12 @@ class Dossier < ApplicationRecord
def motivation def motivation
return nil if !termine? return nil if !termine?
traitements.any? ? traitements.last.motivation : read_attribute(:motivation) traitement&.motivation || read_attribute(:motivation)
end end
def processed_at def processed_at
return nil if !termine? return nil if !termine?
traitements.any? ? traitements.last.processed_at : read_attribute(:processed_at) traitement&.processed_at || read_attribute(:processed_at)
end end
def update_search_terms def update_search_terms
@ -649,6 +662,7 @@ class Dossier < ApplicationRecord
transaction do transaction do
if keep_track_on_deletion? if keep_track_on_deletion?
DeletedDossier.create_from_dossier(self, :expired) DeletedDossier.create_from_dossier(self, :expired)
dossier_operation_logs.destroy_all
log_automatic_dossier_operation(:supprimer, self) log_automatic_dossier_operation(:supprimer, self)
end end
destroy! destroy!
@ -708,14 +722,23 @@ class Dossier < ApplicationRecord
end end
def after_passer_en_construction def after_passer_en_construction
update!(conservation_extension: 0.days) self.conservation_extension = 0.days
update!(en_construction_at: Time.zone.now) if self.en_construction_at.nil? self.en_construction_at = self.traitements
.passer_en_construction
.processed_at
save!
end end
def after_passer_en_instruction(instructeur, disable_notification: false) def after_passer_en_instruction(instructeur, disable_notification: false)
instructeur.follow(self) instructeur.follow(self)
update!(en_instruction_at: Time.zone.now) if self.en_instruction_at.nil? self.en_construction_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_instruction_at = self.traitements
.passer_en_instruction
.processed_at
save!
if !procedure.declarative_accepte? && !disable_notification if !procedure.declarative_accepte? && !disable_notification
NotificationMailer.send_en_instruction_notification(self).deliver_later NotificationMailer.send_en_instruction_notification(self).deliver_later
end end
@ -723,20 +746,32 @@ class Dossier < ApplicationRecord
end end
def after_passer_automatiquement_en_instruction def after_passer_automatiquement_en_instruction
self.en_instruction_at ||= Time.zone.now self.en_construction_close_to_expiration_notice_sent_at = nil
self.declarative_triggered_at = Time.zone.now self.conservation_extension = 0.days
self.en_instruction_at = self.declarative_triggered_at = self.traitements
.passer_en_instruction
.processed_at
save! save!
log_automatic_dossier_operation(:passer_en_instruction) log_automatic_dossier_operation(:passer_en_instruction)
end end
def after_repasser_en_construction(instructeur) def after_repasser_en_construction(instructeur)
update!(conservation_extension: 0.days) self.en_construction_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_construction_at = self.traitements
.passer_en_construction
.processed_at
save!
log_dossier_operation(instructeur, :repasser_en_construction) log_dossier_operation(instructeur, :repasser_en_construction)
end end
def after_repasser_en_instruction(instructeur, disable_notification: false) def after_repasser_en_instruction(instructeur, disable_notification: false)
self.archived = false self.archived = false
self.en_instruction_at = Time.zone.now self.termine_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_instruction_at = self.traitements
.passer_en_instruction
.processed_at
attestation&.destroy attestation&.destroy
save! save!
@ -747,7 +782,10 @@ class Dossier < ApplicationRecord
end end
def after_accepter(instructeur, motivation, justificatif: nil, disable_notification: false) def after_accepter(instructeur, motivation, justificatif: nil, disable_notification: false)
self.traitements.accepter(motivation: motivation, instructeur: instructeur) self.processed_at = self.traitements
.accepter(motivation: motivation, instructeur: instructeur)
.processed_at
save!
if justificatif if justificatif
self.justificatif_motivation.attach(justificatif) self.justificatif_motivation.attach(justificatif)
@ -767,9 +805,10 @@ class Dossier < ApplicationRecord
end end
def after_accepter_automatiquement def after_accepter_automatiquement
self.traitements.accepter_automatiquement self.processed_at = self.en_instruction_at = self.declarative_triggered_at = self.traitements
self.en_instruction_at ||= Time.zone.now .accepter_automatiquement
self.declarative_triggered_at = Time.zone.now .processed_at
save!
if attestation.nil? if attestation.nil?
self.attestation = build_attestation self.attestation = build_attestation
@ -782,7 +821,10 @@ class Dossier < ApplicationRecord
end end
def after_refuser(instructeur, motivation, justificatif: nil, disable_notification: false) def after_refuser(instructeur, motivation, justificatif: nil, disable_notification: false)
self.traitements.refuser(motivation: motivation, instructeur: instructeur) self.processed_at = self.traitements
.refuser(motivation: motivation, instructeur: instructeur)
.processed_at
save!
if justificatif if justificatif
self.justificatif_motivation.attach(justificatif) self.justificatif_motivation.attach(justificatif)
@ -798,7 +840,10 @@ class Dossier < ApplicationRecord
end end
def after_classer_sans_suite(instructeur, motivation, justificatif: nil, disable_notification: false) def after_classer_sans_suite(instructeur, motivation, justificatif: nil, disable_notification: false)
self.traitements.classer_sans_suite(motivation: motivation, instructeur: instructeur) self.processed_at = self.traitements
.classer_sans_suite(motivation: motivation, instructeur: instructeur)
.processed_at
save!
if justificatif if justificatif
self.justificatif_motivation.attach(justificatif) self.justificatif_motivation.attach(justificatif)

View file

@ -13,9 +13,13 @@
class Traitement < ApplicationRecord class Traitement < ApplicationRecord
belongs_to :dossier, optional: false belongs_to :dossier, optional: false
scope :en_construction, -> { where(state: Dossier.states.fetch(:en_construction)) }
scope :en_instruction, -> { where(state: Dossier.states.fetch(:en_instruction)) }
scope :termine, -> { where(state: Dossier::TERMINE) }
scope :termine_close_to_expiration, -> do scope :termine_close_to_expiration, -> do
joins(dossier: :procedure) joins(dossier: :procedure)
.where(state: Dossier::TERMINE) .termine
.where(process_expired: true) .where(process_expired: true)
.where('dossiers.state' => Dossier::TERMINE) .where('dossiers.state' => Dossier::TERMINE)
.where("traitements.processed_at + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: Dossier::INTERVAL_BEFORE_EXPIRATION }) .where("traitements.processed_at + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: Dossier::INTERVAL_BEFORE_EXPIRATION })
@ -23,6 +27,7 @@ class Traitement < ApplicationRecord
scope :for_traitement_time_stats, -> (procedure) do scope :for_traitement_time_stats, -> (procedure) do
includes(:dossier) includes(:dossier)
.termine
.where(dossier: procedure.dossiers) .where(dossier: procedure.dossiers)
.where.not('dossiers.en_construction_at' => nil, :processed_at => nil) .where.not('dossiers.en_construction_at' => nil, :processed_at => nil)
.order(:processed_at) .order(:processed_at)
@ -31,6 +36,7 @@ class Traitement < ApplicationRecord
def self.count_dossiers_termines_by_month(groupe_instructeurs) def self.count_dossiers_termines_by_month(groupe_instructeurs)
last_traitements_per_dossier = Traitement last_traitements_per_dossier = Traitement
.select('max(traitements.processed_at) as processed_at') .select('max(traitements.processed_at) as processed_at')
.termine
.where(dossier: Dossier.state_termine.where(groupe_instructeur: groupe_instructeurs)) .where(dossier: Dossier.state_termine.where(groupe_instructeur: groupe_instructeurs))
.group(:dossier_id) .group(:dossier_id)
.to_sql .to_sql

View file

@ -297,13 +297,12 @@ class TypeDeChamp < ApplicationRecord
# otherwise return all types_de_champ in their latest state # otherwise return all types_de_champ in their latest state
types_de_champ = TypeDeChamp types_de_champ = TypeDeChamp
.fillable .fillable
.joins(:parent) .joins(parent: :revision_types_de_champ)
.where(parent: { stable_id: stable_id }) .where(parent: { stable_id: stable_id }, revision_types_de_champ: { revision_id: revision })
TypeDeChamp TypeDeChamp
.where(id: types_de_champ.group(:stable_id).select('MAX(types_de_champ.id)')) .where(id: types_de_champ.group(:stable_id).select('MAX(types_de_champ.id)'))
.joins(parent: :revision_types_de_champ) .order(:order_place, id: :desc)
.order(:order_place, 'procedure_revision_types_de_champ.revision_id': :desc)
end end
end end

View file

@ -0,0 +1,22 @@
namespace :after_party do
desc 'Deployment task: set_dossiers_processed_at'
task set_dossiers_processed_at: :environment do
puts "Running deploy task 'set_dossiers_processed_at'"
dossiers = Dossier.termine.includes(:traitement)
progress = ProgressReport.new(dossiers.count)
dossiers.find_each do |dossier|
if dossier.processed_at != dossier.traitement.processed_at
dossier.update_column(:processed_at, dossier.traitement.processed_at)
end
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -340,15 +340,11 @@ describe Instructeurs::DossiersController, type: :controller do
end end
before do before do
Timecop.freeze(Time.zone.now)
expect_any_instance_of(AttestationTemplate) expect_any_instance_of(AttestationTemplate)
.to receive(:attestation_for) .to receive(:attestation_for)
.with(have_attributes(motivation: "Yallah", processed_at: Time.zone.now)) .with(have_attributes(motivation: "Yallah"))
end end
after { Timecop.return }
it { subject } it { subject }
end end

View file

@ -387,13 +387,18 @@ describe Dossier do
it { expect(dossier.state).to eq(Dossier.states.fetch(:en_construction)) } it { expect(dossier.state).to eq(Dossier.states.fetch(:en_construction)) }
it { expect(dossier.en_construction_at).to eq(beginning_of_day) } it { expect(dossier.en_construction_at).to eq(beginning_of_day) }
it { expect(dossier.traitement.state).to eq(Dossier.states.fetch(:en_construction)) }
it { expect(dossier.traitement.processed_at).to eq(beginning_of_day) }
it 'should keep first en_construction_at date' do it 'should keep first en_construction_at date' do
Timecop.return Timecop.return
dossier.passer_en_instruction!(instructeur) dossier.passer_en_instruction!(instructeur)
dossier.repasser_en_construction!(instructeur) dossier.repasser_en_construction!(instructeur)
expect(dossier.en_construction_at).to eq(beginning_of_day) expect(dossier.traitements.size).to eq(3)
expect(dossier.traitements.first.processed_at).to eq(beginning_of_day)
expect(dossier.traitement.processed_at.round).to eq(dossier.en_construction_at.round)
expect(dossier.en_construction_at).to be > beginning_of_day
end end
end end
@ -408,13 +413,18 @@ describe Dossier do
it { expect(dossier.state).to eq(Dossier.states.fetch(:en_instruction)) } it { expect(dossier.state).to eq(Dossier.states.fetch(:en_instruction)) }
it { expect(dossier.en_instruction_at).to eq(beginning_of_day) } it { expect(dossier.en_instruction_at).to eq(beginning_of_day) }
it { expect(dossier.traitement.state).to eq(Dossier.states.fetch(:en_instruction)) }
it { expect(dossier.traitement.processed_at).to eq(beginning_of_day) }
it 'should keep first en_instruction_at date if dossier is set to en_construction again' do it 'should keep first en_instruction_at date if dossier is set to en_construction again' do
Timecop.return Timecop.return
dossier.repasser_en_construction!(instructeur) dossier.repasser_en_construction!(instructeur)
dossier.passer_en_instruction!(instructeur) dossier.passer_en_instruction!(instructeur)
expect(dossier.en_instruction_at).to eq(beginning_of_day) expect(dossier.traitements.size).to eq(3)
expect(dossier.traitements.first.processed_at).to eq(beginning_of_day)
expect(dossier.traitement.processed_at.round).to eq(dossier.en_instruction_at.round)
expect(dossier.en_instruction_at).to be > beginning_of_day
end end
end end
@ -427,8 +437,9 @@ describe Dossier do
end end
it { expect(dossier.state).to eq(Dossier.states.fetch(:accepte)) } it { expect(dossier.state).to eq(Dossier.states.fetch(:accepte)) }
it { expect(dossier.traitements.last.processed_at).to eq(beginning_of_day) }
it { expect(dossier.processed_at).to eq(beginning_of_day) } it { expect(dossier.processed_at).to eq(beginning_of_day) }
it { expect(dossier.traitement.state).to eq(Dossier.states.fetch(:accepte)) }
it { expect(dossier.traitement.processed_at).to eq(beginning_of_day) }
end end
context 'when dossier is refuse' do context 'when dossier is refuse' do
@ -441,6 +452,8 @@ describe Dossier do
it { expect(dossier.state).to eq(Dossier.states.fetch(:refuse)) } it { expect(dossier.state).to eq(Dossier.states.fetch(:refuse)) }
it { expect(dossier.processed_at).to eq(beginning_of_day) } it { expect(dossier.processed_at).to eq(beginning_of_day) }
it { expect(dossier.traitement.state).to eq(Dossier.states.fetch(:refuse)) }
it { expect(dossier.traitement.processed_at).to eq(beginning_of_day) }
end end
context 'when dossier is sans_suite' do context 'when dossier is sans_suite' do
@ -453,6 +466,8 @@ describe Dossier do
it { expect(dossier.state).to eq(Dossier.states.fetch(:sans_suite)) } it { expect(dossier.state).to eq(Dossier.states.fetch(:sans_suite)) }
it { expect(dossier.processed_at).to eq(beginning_of_day) } it { expect(dossier.processed_at).to eq(beginning_of_day) }
it { expect(dossier.traitement.state).to eq(Dossier.states.fetch(:sans_suite)) }
it { expect(dossier.traitement.processed_at).to eq(beginning_of_day) }
end end
end end
@ -997,7 +1012,7 @@ describe Dossier do
end end
describe '#passer_en_instruction!' do describe '#passer_en_instruction!' do
let(:dossier) { create(:dossier, :en_construction) } let(:dossier) { create(:dossier, :en_construction, en_construction_close_to_expiration_notice_sent_at: Time.zone.now) }
let(:last_operation) { dossier.dossier_operation_logs.last } let(:last_operation) { dossier.dossier_operation_logs.last }
let(:operation_serialized) { JSON.parse(last_operation.serialized.download) } let(:operation_serialized) { JSON.parse(last_operation.serialized.download) }
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
@ -1006,6 +1021,7 @@ describe Dossier do
it { expect(dossier.state).to eq('en_instruction') } it { expect(dossier.state).to eq('en_instruction') }
it { expect(dossier.followers_instructeurs).to include(instructeur) } it { expect(dossier.followers_instructeurs).to include(instructeur) }
it { expect(dossier.en_construction_close_to_expiration_notice_sent_at).to be_nil }
it { expect(last_operation.operation).to eq('passer_en_instruction') } it { expect(last_operation.operation).to eq('passer_en_instruction') }
it { expect(last_operation.automatic_operation?).to be_falsey } it { expect(last_operation.automatic_operation?).to be_falsey }
it { expect(operation_serialized['operation']).to eq('passer_en_instruction') } it { expect(operation_serialized['operation']).to eq('passer_en_instruction') }
@ -1014,7 +1030,7 @@ describe Dossier do
end end
describe '#passer_automatiquement_en_instruction!' do describe '#passer_automatiquement_en_instruction!' do
let(:dossier) { create(:dossier, :en_construction) } let(:dossier) { create(:dossier, :en_construction, en_construction_close_to_expiration_notice_sent_at: Time.zone.now) }
let(:last_operation) { dossier.dossier_operation_logs.last } let(:last_operation) { dossier.dossier_operation_logs.last }
let(:operation_serialized) { JSON.parse(last_operation.serialized.download) } let(:operation_serialized) { JSON.parse(last_operation.serialized.download) }
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
@ -1022,6 +1038,7 @@ describe Dossier do
before { dossier.passer_automatiquement_en_instruction! } before { dossier.passer_automatiquement_en_instruction! }
it { expect(dossier.followers_instructeurs).not_to include(instructeur) } it { expect(dossier.followers_instructeurs).not_to include(instructeur) }
it { expect(dossier.en_construction_close_to_expiration_notice_sent_at).to be_nil }
it { expect(last_operation.operation).to eq('passer_en_instruction') } it { expect(last_operation.operation).to eq('passer_en_instruction') }
it { expect(last_operation.automatic_operation?).to be_truthy } it { expect(last_operation.automatic_operation?).to be_truthy }
it { expect(operation_serialized['operation']).to eq('passer_en_instruction') } it { expect(operation_serialized['operation']).to eq('passer_en_instruction') }
@ -1119,7 +1136,7 @@ describe Dossier do
end end
describe '#repasser_en_instruction!' do describe '#repasser_en_instruction!' do
let(:dossier) { create(:dossier, :refuse, :with_attestation, archived: true) } let(:dossier) { create(:dossier, :refuse, :with_attestation, archived: true, termine_close_to_expiration_notice_sent_at: Time.zone.now) }
let!(:instructeur) { create(:instructeur) } let!(:instructeur) { create(:instructeur) }
let(:last_operation) { dossier.dossier_operation_logs.last } let(:last_operation) { dossier.dossier_operation_logs.last }
@ -1136,6 +1153,7 @@ describe Dossier do
it { expect(dossier.processed_at).to be_nil } it { expect(dossier.processed_at).to be_nil }
it { expect(dossier.motivation).to be_nil } it { expect(dossier.motivation).to be_nil }
it { expect(dossier.attestation).to be_nil } it { expect(dossier.attestation).to be_nil }
it { expect(dossier.termine_close_to_expiration_notice_sent_at).to be_nil }
it { expect(last_operation.operation).to eq('repasser_en_instruction') } it { expect(last_operation.operation).to eq('repasser_en_instruction') }
it { expect(JSON.parse(last_operation.serialized.download)['author']['email']).to eq(instructeur.email) } it { expect(JSON.parse(last_operation.serialized.download)['author']['email']).to eq(instructeur.email) }
it { expect(DossierMailer).to have_received(:notify_revert_to_instruction).with(dossier) } it { expect(DossierMailer).to have_received(:notify_revert_to_instruction).with(dossier) }

View file

@ -342,13 +342,22 @@ describe ProcedureExportService do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle_for_export]) expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle_for_export])
end end
it 'should have headers' do context 'with cloned procedure' do
expect(repetition_sheet.headers).to eq([ let(:other_parent) { create(:type_de_champ_repetition, stable_id: champ_repetition.stable_id) }
"Dossier ID",
"Ligne", before do
"Nom", create(:procedure_revision_type_de_champ, type_de_champ: other_parent, revision: create(:procedure).active_revision)
"Age" create(:type_de_champ, parent: other_parent)
]) end
it 'should have headers' do
expect(repetition_sheet.headers).to eq([
"Dossier ID",
"Ligne",
"Nom",
"Age"
])
end
end end
it 'should have data' do it 'should have data' do