Merge pull request #9270 from demarches-simplifiees/create-dossier-assignments

ETQ instructeur je vois les réaffectations d'un dossier
This commit is contained in:
Paul Chavard 2023-07-18 15:39:12 +00:00 committed by GitHub
commit 6d8a0506e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 228 additions and 30 deletions

View file

@ -743,6 +743,7 @@ Rails/CreateTableWithTimestamps:
- db/migrate/2017*.rb
- db/migrate/2018*.rb
- db/migrate/20200630140356_create_traitements.rb
- db/migrate/20230630091637_create_dossier_assignments.rb
Rails/Date:
Enabled: false

View file

@ -200,7 +200,7 @@ module Administrateurs
target_group = procedure.groupe_instructeurs.find(params[:target_group])
reaffecter_bulk_messages(target_group)
groupe_instructeur.dossiers.find_each do |dossier|
dossier.assign_to_groupe_instructeur(target_group, current_administrateur)
dossier.assign_to_groupe_instructeur(target_group, DossierAssignment.modes.fetch(:manual), current_administrateur)
end
flash[:notice] = "Les dossiers du groupe « #{groupe_instructeur.label} » ont été réaffectés au groupe « #{target_group.label} »."
@ -210,7 +210,7 @@ module Administrateurs
def reaffecter_all_dossiers_to_defaut_groupe
procedure.groupe_instructeurs_but_defaut.each do |gi|
gi.dossiers.find_each do |dossier|
dossier.assign_to_groupe_instructeur(procedure.defaut_groupe_instructeur, current_administrateur)
dossier.assign_to_groupe_instructeur(procedure.defaut_groupe_instructeur, DossierAssignment.modes.fetch(:manual), current_administrateur)
end
end
end

View file

@ -91,6 +91,7 @@ module Instructeurs
@avis_emails = dossier.experts.map(&:email)
@invites_emails = dossier.invites.map(&:email)
@potential_recipients = dossier.groupe_instructeur.instructeurs.reject { |g| g == current_instructeur }
@manual_assignments = dossier.dossier_assignments.manual.includes(:groupe_instructeur, :previous_groupe_instructeur)
end
def send_to_instructeurs
@ -362,9 +363,7 @@ module Instructeurs
.procedure
.groupe_instructeurs.find(params[:groupe_instructeur_id])
dossier.assign_to_groupe_instructeur(new_group)
dossier.update!(forced_groupe_instructeur: true)
dossier.assign_to_groupe_instructeur(new_group, DossierAssignment.modes.fetch(:manual), current_instructeur)
flash.notice = t('instructeurs.dossiers.reaffectation', dossier_id: dossier.id, label: new_group.label)
redirect_to instructeur_procedure_path(procedure)

View file

@ -542,8 +542,6 @@ module Users
errors += format_errors(errors: @dossier.errors)
errors += format_errors(errors: @dossier.check_mandatory_and_visible_champs)
RoutingEngine.compute(@dossier)
errors
end

View file

@ -11,7 +11,7 @@ module Mutations
field :errors, [Types::ValidationErrorType], null: true
def resolve(dossier:, groupe_instructeur:)
dossier.assign_to_groupe_instructeur(groupe_instructeur)
dossier.assign_to_groupe_instructeur(groupe_instructeur, DossierAssignment.modes.fetch(:manual), current_administrateur)
{ dossier: }
end

View file

@ -69,7 +69,6 @@ module DossierCloneConcern
diff = make_diff(editing_fork)
apply_diff(diff)
touch(:last_champ_updated_at)
assign_to_groupe_instructeur(editing_fork.groupe_instructeur)
end
reload
update_search_terms_later

View file

@ -155,6 +155,8 @@ class Dossier < ApplicationRecord
has_one :traitement, -> { order(processed_at: :desc) }, inverse_of: false
has_many :dossier_operation_logs, -> { order(:created_at) }, inverse_of: :dossier
has_many :dossier_assignments, -> { order(:assigned_at) }, inverse_of: :dossier, dependent: :destroy
has_one :dossier_assignment, -> { order(assigned_at: :desc) }, inverse_of: false
belongs_to :groupe_instructeur, optional: true
belongs_to :revision, class_name: 'ProcedureRevision', optional: false
@ -474,7 +476,6 @@ class Dossier < ApplicationRecord
validates :user, presence: true, if: -> { deleted_user_email_never_send.nil? }, unless: -> { prefilled }
validates :individual, presence: true, if: -> { revision.procedure.for_individual? }
validates :groupe_instructeur, presence: true, if: -> { !brouillon? }
validates_associated :prefilled_champs_public, on: :prefilling
@ -686,13 +687,17 @@ class Dossier < ApplicationRecord
procedure.discarded? || (brouillon? && !procedure.dossier_can_transition_to_en_construction?)
end
def assign_to_groupe_instructeur(groupe_instructeur, author = nil)
def assign_to_groupe_instructeur(groupe_instructeur, mode, author = nil)
return if groupe_instructeur.present? && groupe_instructeur.procedure != procedure
return if self.groupe_instructeur == groupe_instructeur
previous_groupe_instructeur = self.groupe_instructeur
update!(groupe_instructeur:, groupe_instructeur_updated_at: Time.zone.now)
update!(forced_groupe_instructeur: true) if mode == DossierAssignment.modes.fetch(:manual)
if !brouillon?
create_assignment(mode, previous_groupe_instructeur, groupe_instructeur, author&.email)
unfollow_stale_instructeurs
if author.present?
log_dossier_operation(author, :changer_groupe_instructeur, self)
@ -900,6 +905,7 @@ class Dossier < ApplicationRecord
MailTemplatePresenterService.create_commentaire_for_state(self)
NotificationMailer.send_en_construction_notification(self).deliver_later
procedure.compute_dossiers_count
RoutingEngine.compute(self)
end
def after_passer_en_instruction(h)
@ -1310,6 +1316,19 @@ class Dossier < ApplicationRecord
(sva_svr_decision_on - Date.current).to_i
end
def create_assignment(mode, previous_groupe_instructeur, groupe_instructeur, instructeur_email = nil)
DossierAssignment.create!(
dossier_id: self.id,
mode: mode,
previous_groupe_instructeur_id: previous_groupe_instructeur&.id,
groupe_instructeur_id: groupe_instructeur.id,
previous_groupe_instructeur_label: previous_groupe_instructeur&.label,
groupe_instructeur_label: groupe_instructeur.label,
assigned_at: Time.zone.now,
assigned_by: instructeur_email
)
end
private
def create_missing_traitemets

View file

@ -0,0 +1,34 @@
# == Schema Information
#
# Table name: dossier_assignments
#
# id :bigint not null, primary key
# assigned_at :datetime not null
# assigned_by :string
# groupe_instructeur_label :string
# mode :string not null
# previous_groupe_instructeur_label :string
# dossier_id :bigint not null
# groupe_instructeur_id :bigint
# previous_groupe_instructeur_id :bigint
#
class DossierAssignment < ApplicationRecord
belongs_to :dossier
belongs_to :groupe_instructeur, optional: true, inverse_of: :assignments
belongs_to :previous_groupe_instructeur, class_name: 'GroupeInstructeur', optional: true, inverse_of: :previous_assignments
enum mode: {
auto: 'auto',
manual: 'manual'
}
scope :manual, -> { where(mode: :manual) }
def groupe_instructeur_label
@groupe_instructeur_label ||= groupe_instructeur&.label.presence || read_attribute(:groupe_instructeur_label)
end
def previous_groupe_instructeur_label
@previous_groupe_instructeur_label ||= previous_groupe_instructeur&.label.presence || read_attribute(:previous_groupe_instructeur_label)
end
end

View file

@ -19,6 +19,8 @@ class GroupeInstructeur < ApplicationRecord
has_many :dossiers
has_many :deleted_dossiers
has_many :batch_operations, through: :dossiers, source: :batch_operations
has_many :assignments, class_name: 'DossierAssignment', dependent: :nullify, inverse_of: :groupe_instructeur
has_many :previous_assignments, class_name: 'DossierAssignment', dependent: :nullify, inverse_of: :previous_groupe_instructeur
has_and_belongs_to_many :exports, dependent: :destroy
has_and_belongs_to_many :bulk_messages, dependent: :destroy

View file

@ -5,7 +5,9 @@ module RoutingEngine
matching_groupe = dossier.procedure.groupe_instructeurs.active.reject(&:invalid_rule?).find do |gi|
gi.routing_rule&.compute(dossier.champs)
end
matching_groupe ||= dossier.procedure.defaut_groupe_instructeur
dossier.assign_to_groupe_instructeur(matching_groupe)
dossier.assign_to_groupe_instructeur(matching_groupe, DossierAssignment.modes.fetch(:auto))
end
end

View file

@ -16,11 +16,11 @@
%th{ colspan: 2 }= t(".existing_groupe", count: @groupes_instructeurs.total_count)
%tbody
- @groupes_instructeurs.each do |group|
%tr
%td= group.label
%td.actions= button_to 'Réaffecter les dossiers à ce groupe',
.flex.justify-between.align-center.fr-mb-2w
%p.fr-mb-0= group.label
= button_to 'Réaffecter les dossiers à ce groupe',
reaffecter_admin_procedure_groupe_instructeur_path(:target_group => group),
{ class: 'button',
{ class: 'fr-btn fr-btn--secondary fr-btn--sm',
data: { confirm: "Êtes-vous sûr de vouloir réaffecter les dossiers du groupe « #{@groupe_instructeur.label} » vers le groupe  « #{group.label} » ?" } }
= paginate @groupes_instructeurs, views_prefix: 'shared'

View file

@ -0,0 +1,16 @@
.tab-title Réaffectations
- if manual_assignments.any?
%ul.tab-list
- manual_assignments.each do |assignment|
%li
- assigned_at = l(assignment.assigned_at, format: '%d %B %Y à %R')
- assigned_by = assignment.assigned_by
- if assigned_by.present?
= "Le #{assigned_at}, #{assigned_by} a réaffecté ce dossier du groupe « #{assignment.previous_groupe_instructeur_label} » au groupe « #{assignment.groupe_instructeur_label} »"
- else
= "Le #{assigned_at}, ce dossier a été réaffecté du groupe « #{assignment.previous_groupe_instructeur_label} » au groupe « #{assignment.groupe_instructeur_label} »"
- elsif dossier.forced_groupe_instructeur
%p.tab-paragraph Ce dossier a été réaffecté au groupe « #{dossier.groupe_instructeur.label} »
- else
%p.tab-paragraph Ce dossier n'a pas été réaffecté

View file

@ -16,6 +16,7 @@
= render partial: 'instructeurs/dossiers/decisions_rendues_block', locals: { traitements: @dossier.traitements }
= render partial: 'instructeurs/dossiers/reaffectations_block', locals: { manual_assignments: @manual_assignments, dossier: @dossier }
- if @dossier.archived? && @dossier.archived_at.present?
= render partial: 'instructeurs/dossiers/archived_block', locals: @dossier.slice(:archived_by, :archived_at)

View file

@ -7,18 +7,18 @@
.card
.card-title Réaffectation du dossier nº #{@dossier.id} du groupe « #{@groupe_instructeur.label} »
%p
Vous pouvez réaffecter le dossier nº #{@dossier.id} à l'un des groupes d'instructeurs suivants.
Vous pouvez réaffecter le dossier nº #{@dossier.id} à lun des groupes dinstructeurs suivants.
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 }= t("instructeurs.dossiers.existing_groupe", count: @groupes_instructeurs.total_count)
%tbody
- @groupes_instructeurs.each do |group|
%tr
%td= group.label
%td.actions= button_to 'Réaffecter le dossier à ce groupe',
.flex.justify-between.align-center.fr-mb-2w
%p.fr-mb-0= group.label
= button_to 'Réaffecter le dossier à ce groupe',
reaffecter_instructeur_dossier_path(procedure_id: @dossier.procedure.id, dossier_id: @dossier.id, groupe_instructeur_id: group.id),
{ class: 'button',
{ class: 'fr-btn fr-btn--secondary fr-btn--sm',
data: { confirm: "Êtes-vous sûr de vouloir réaffecter le dossier nº #{@dossier.id} du groupe « #{@groupe_instructeur.label} » vers le groupe  « #{group.label} » ?" } }
= paginate @groupes_instructeurs, views_prefix: 'shared'

View file

@ -0,0 +1,14 @@
class CreateDossierAssignments < ActiveRecord::Migration[7.0]
def change
create_table :dossier_assignments do |t|
t.references :dossier, foreign_key: true, null: false
t.string :mode, null: false
t.bigint :groupe_instructeur_id
t.bigint :previous_groupe_instructeur_id
t.string :groupe_instructeur_label
t.string :previous_groupe_instructeur_label
t.string :assigned_by
t.timestamp :assigned_at, null: false
end
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_07_18_113720) do
ActiveRecord::Schema[7.0].define(version: 2023_07_18_113820) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@ -307,6 +307,18 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_18_113720) do
t.index ["procedure_id"], name: "index_deleted_dossiers_on_procedure_id"
end
create_table "dossier_assignments", force: :cascade do |t|
t.datetime "assigned_at", precision: nil, null: false
t.string "assigned_by"
t.bigint "dossier_id", null: false
t.bigint "groupe_instructeur_id"
t.string "groupe_instructeur_label"
t.string "mode", null: false
t.bigint "previous_groupe_instructeur_id"
t.string "previous_groupe_instructeur_label"
t.index ["dossier_id"], name: "index_dossier_assignments_on_dossier_id"
end
create_table "dossier_batch_operations", force: :cascade do |t|
t.bigint "batch_operation_id", null: false
t.datetime "created_at", precision: 6, null: false
@ -1030,6 +1042,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_18_113720) do
add_foreign_key "commentaires", "dossiers"
add_foreign_key "commentaires", "experts"
add_foreign_key "commentaires", "instructeurs"
add_foreign_key "dossier_assignments", "dossiers"
add_foreign_key "dossier_batch_operations", "batch_operations"
add_foreign_key "dossier_batch_operations", "dossiers"
add_foreign_key "dossier_corrections", "commentaires"

View file

@ -208,6 +208,9 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do
it { expect(response).to redirect_to(admin_procedure_groupe_instructeurs_path(procedure)) }
it { expect(gi_1_2.dossiers.last.id).to be(dossier12.id) }
it { expect(dossier12.groupe_instructeur.id).to be(gi_1_2.id) }
it { expect(dossier12.dossier_assignment.dossier_id).to be(dossier12.id) }
it { expect(dossier12.dossier_assignment.groupe_instructeur_id).to be(gi_1_2.id) }
it { expect(dossier12.dossier_assignment.assigned_by).to eq(admin.email) }
it { expect(bulk_message.groupe_instructeurs).to contain_exactly(gi_1_2, gi_1_3) }
end
@ -231,6 +234,30 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do
end
end
describe '#destroy_all_groups_but_defaut' do
let!(:dossierA) { create(:dossier, :en_construction, :with_individual, procedure: procedure, groupe_instructeur: gi_1_2) }
let!(:dossierB) { create(:dossier, :en_construction, :with_individual, procedure: procedure, groupe_instructeur: gi_1_2) }
before do
post :destroy_all_groups_but_defaut,
params: {
procedure_id: procedure.id
}
dossierA.reload
dossierB.reload
end
it do
expect(dossierA.groupe_instructeur.id).to be(procedure.defaut_groupe_instructeur.id)
expect(dossierB.groupe_instructeur.id).to be(procedure.defaut_groupe_instructeur.id)
expect(dossierA.dossier_assignment.dossier_id).to be(dossierA.id)
expect(dossierB.dossier_assignment.dossier_id).to be(dossierB.id)
expect(dossierA.dossier_assignment.groupe_instructeur_id).to be(procedure.defaut_groupe_instructeur.id)
expect(dossierB.dossier_assignment.groupe_instructeur_id).to be(procedure.defaut_groupe_instructeur.id)
expect(dossierA.dossier_assignment.assigned_by).to eq(admin.email)
expect(dossierB.dossier_assignment.assigned_by).to eq(admin.email)
end
end
describe '#update' do
let(:new_name) { 'nouveau nom du groupe' }
let!(:procedure_non_routee) { create(:procedure, :published, :for_individual, administrateurs: [admin]) }

View file

@ -1179,16 +1179,21 @@ describe Instructeurs::DossiersController, type: :controller do
it do
expect(response).to have_http_status(:ok)
expect(response.body).to include("Vous pouvez réaffecter le dossier nº #{dossier.id} à l'un des groupes d'instructeurs suivants.")
expect(response.body).to include("Vous pouvez réaffecter le dossier nº #{dossier.id} à lun des groupes dinstructeurs suivants.")
expect(response.body).to include('2 groupes existent')
end
end
describe '#reaffecter' do
let!(:gi_1) { procedure.groupe_instructeurs.first }
let!(:gi_2) { GroupeInstructeur.create(label: 'deuxième groupe', procedure: procedure) }
let!(:dossier) { create(:dossier, :en_construction, procedure: procedure, groupe_instructeur: procedure.groupe_instructeurs.first) }
let!(:dossier) { create(:dossier, :en_construction, :with_individual, procedure: procedure, groupe_instructeur: gi_1) }
let!(:new_instructeur) { create(:instructeur) }
before do
gi_1.instructeurs << new_instructeur
new_instructeur.followed_dossiers << dossier
post :reaffecter,
params: {
procedure_id: procedure.id,
@ -1200,8 +1205,39 @@ describe Instructeurs::DossiersController, type: :controller do
it do
expect(dossier.reload.groupe_instructeur.id).to eq(gi_2.id)
expect(dossier.forced_groupe_instructeur).to be_truthy
expect(dossier.followers_instructeurs).to eq []
expect(dossier.dossier_assignment.previous_groupe_instructeur_id).to eq(gi_1.id)
expect(dossier.dossier_assignment.previous_groupe_instructeur_label).to eq(gi_1.label)
expect(dossier.dossier_assignment.groupe_instructeur_id).to eq(gi_2.id)
expect(dossier.dossier_assignment.groupe_instructeur_label).to eq(gi_2.label)
expect(dossier.dossier_assignment.mode).to eq('manual')
expect(dossier.dossier_assignment.assigned_by).to eq(instructeur.email)
expect(response).to redirect_to(instructeur_procedure_path(procedure))
expect(flash.notice).to eq("Le dossier nº #{dossier.id} a été réaffecté au groupe dinstructeurs « deuxième groupe ».")
end
end
describe '#personnes_impliquees' do
let!(:gi_1) { procedure.groupe_instructeurs.first }
let!(:gi_2) { GroupeInstructeur.create(label: 'deuxième groupe', procedure: procedure) }
let!(:dossier) { create(:dossier, :en_construction, :with_individual, procedure: procedure, groupe_instructeur: gi_1) }
let!(:new_instructeur) { create(:instructeur) }
before do
gi_1.instructeurs << new_instructeur
gi_2.instructeurs << instructeur
new_instructeur.followed_dossiers << dossier
dossier.assign_to_groupe_instructeur(gi_2, DossierAssignment.modes.fetch(:manual), new_instructeur)
get :personnes_impliquees,
params: {
procedure_id: procedure.id,
dossier_id: dossier.id
}
end
it do
expect(response.body).to include('a réaffecté ce dossier du groupe « défaut » au groupe « deuxième groupe »')
end
end
end

View file

@ -221,7 +221,7 @@ RSpec.describe DossierCloneConcern do
context 'with updated groupe instructeur' do
before {
dossier.update!(groupe_instructeur: create(:groupe_instructeur))
forked_dossier.assign_to_groupe_instructeur(dossier.procedure.defaut_groupe_instructeur)
forked_dossier.assign_to_groupe_instructeur(dossier.procedure.defaut_groupe_instructeur, DossierAssignment.modes.fetch(:manual))
}
it { is_expected.to eq(added: [], updated: [], removed: []) }

View file

@ -0,0 +1,37 @@
require 'rails_helper'
RSpec.describe DossierAssignment, type: :model do
include Logic
before { Flipper.enable(:routing_rules, procedure) }
context 'Assignment from routing engine' do
let(:procedure) do
create(:procedure,
types_de_champ_public: [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }]).tap do |p|
p.groupe_instructeurs.create(label: 'a second group')
p.groupe_instructeurs.create(label: 'a third group')
end
end
let(:drop_down_tdc) { procedure.draft_revision.types_de_champ.first }
let(:dossier) { create(:dossier, :en_construction, procedure:).tap { _1.update(groupe_instructeur_id: nil) } }
before do
RoutingEngine.compute(dossier)
dossier.reload
end
it 'creates a dossier assignment with right attributes' do
expect(dossier.dossier_assignments.count).to eq 1
expect(dossier.dossier_assignment.mode).to eq 'auto'
expect(dossier.dossier_assignment.dossier_id).to eq dossier.id
expect(dossier.dossier_assignment.groupe_instructeur_id).to eq dossier.groupe_instructeur.id
expect(dossier.dossier_assignment.groupe_instructeur_label).to eq dossier.groupe_instructeur.label
expect(dossier.dossier_assignment.previous_groupe_instructeur_id).to be nil
expect(dossier.dossier_assignment.previous_groupe_instructeur_label).to be nil
expect(dossier.dossier_assignment.assigned_by).to be nil
end
end
end

View file

@ -578,12 +578,12 @@ describe Dossier, type: :model do
let(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
it "can change groupe instructeur" do
dossier.assign_to_groupe_instructeur(new_groupe_instructeur_new_procedure)
dossier.assign_to_groupe_instructeur(new_groupe_instructeur_new_procedure, DossierAssignment.modes.fetch(:auto))
expect(dossier.groupe_instructeur).not_to eq(new_groupe_instructeur_new_procedure)
end
it "can not change groupe instructeur if new groupe is from another procedure" do
dossier.assign_to_groupe_instructeur(new_groupe_instructeur)
dossier.assign_to_groupe_instructeur(new_groupe_instructeur, DossierAssignment.modes.fetch(:auto))
expect(dossier.groupe_instructeur).to eq(new_groupe_instructeur)
end
end
@ -603,7 +603,7 @@ describe Dossier, type: :model do
it "unfollows stale instructeurs when groupe instructeur change" do
instructeur.follow(dossier)
instructeur2.follow(dossier)
dossier.reload.assign_to_groupe_instructeur(new_groupe_instructeur, procedure.administrateurs.first)
dossier.reload.assign_to_groupe_instructeur(new_groupe_instructeur, DossierAssignment.modes.fetch(:auto), procedure.administrateurs.first)
expect(dossier.reload.followers_instructeurs).not_to include(instructeur)
expect(dossier.reload.followers_instructeurs).to include(instructeur2)

View file

@ -209,7 +209,7 @@ describe Instructeur, type: :model do
context 'when there is a modification on groupe instructeur' do
let(:groupe_instructeur) { create(:groupe_instructeur, instructeurs: [instructeur], procedure: dossier.procedure) }
before { dossier.assign_to_groupe_instructeur(groupe_instructeur) }
before { dossier.assign_to_groupe_instructeur(groupe_instructeur, DossierAssignment.modes.fetch(:auto)) }
it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false }) }
end