diff --git a/app/controllers/manager/procedures_controller.rb b/app/controllers/manager/procedures_controller.rb index dd0dcf0f1..9ca75b503 100644 --- a/app/controllers/manager/procedures_controller.rb +++ b/app/controllers/manager/procedures_controller.rb @@ -41,7 +41,7 @@ module Manager def export_mail_brouillons dossiers = procedure.dossiers.state_brouillon.includes(:user) - emails = dossiers.map { |d| d.user.email }.sort.uniq + emails = dossiers.map { |dossier| dossier.user_email_for(:display) }.sort.uniq date = Time.zone.now.strftime('%d-%m-%Y') send_data(emails.join("\n"), :filename => "brouillons-#{procedure.id}-au-#{date}.csv") end diff --git a/app/graphql/types/dossier_type.rb b/app/graphql/types/dossier_type.rb index 7f340ffc3..57811a75e 100644 --- a/app/graphql/types/dossier_type.rb +++ b/app/graphql/types/dossier_type.rb @@ -54,7 +54,11 @@ module Types end def usager - Loaders::Record.for(User).load(object.user_id) + if object.user_deleted? + { email: object.user_email_for(:display), id: -1 } + else + Loaders::Record.for(User).load(object.user_id) + end end def groupe_instructeur diff --git a/app/mailers/dossier_mailer.rb b/app/mailers/dossier_mailer.rb index 285802797..29e035ffc 100644 --- a/app/mailers/dossier_mailer.rb +++ b/app/mailers/dossier_mailer.rb @@ -13,7 +13,7 @@ class DossierMailer < ApplicationMailer subject = "Retrouvez votre brouillon pour la démarche « #{dossier.procedure.libelle} »" - mail(from: NO_REPLY_EMAIL, to: dossier.user.email, subject: subject) do |format| + mail(from: NO_REPLY_EMAIL, to: dossier.user_email_for(:notification), subject: subject) do |format| format.html { render layout: 'mailers/notifications_layout' } end end @@ -25,7 +25,7 @@ class DossierMailer < ApplicationMailer subject = "Nouveau message pour votre dossier nº #{dossier.id} (#{dossier.procedure.libelle})" - mail(from: NO_REPLY_EMAIL, to: dossier.user.email, subject: subject) do |format| + mail(from: NO_REPLY_EMAIL, to: dossier.user_email_for(:notification), subject: subject) do |format| format.html { render layout: 'mailers/notifications_layout' } end end @@ -49,7 +49,7 @@ class DossierMailer < ApplicationMailer subject = "Votre dossier nº #{@dossier.id} est en train d'être réexaminé" - mail(from: NO_REPLY_EMAIL, to: dossier.user.email, subject: subject) do |format| + mail(from: NO_REPLY_EMAIL, to: dossier.user_email_for(:notification), subject: subject) do |format| format.html { render layout: 'mailers/notifications_layout' } end end @@ -139,7 +139,7 @@ class DossierMailer < ApplicationMailer @subject = "Attention : votre dossier n'est pas déposé." @dossier = dossier - mail(to: dossier.user.email, subject: @subject) + mail(to: dossier.user_email_for(:notification), subject: @subject) end protected diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 829c7ce47..d958d781b 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -8,6 +8,8 @@ class NotificationMailer < ApplicationMailer include ActionView::Helpers::SanitizeHelper + before_action :prevent_delivery_to_deleted_users + helper ServiceHelper helper MailerHelper @@ -36,8 +38,12 @@ class NotificationMailer < ApplicationMailer private + def prevent_delivery_to_deleted_users + !@dossier.user_deleted? + end + def send_notification(dossier, mail_template) - email = dossier.user.email + email = dossier.user_email_for(:notification) subject = mail_template.subject_for_dossier(dossier) body = mail_template.body_for_dossier(dossier) diff --git a/app/models/dossier.rb b/app/models/dossier.rb index f8023d563..53ec3ff38 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -8,6 +8,7 @@ # autorisation_donnees :boolean # brouillon_close_to_expiration_notice_sent_at :datetime # conservation_extension :interval default(0 seconds) +# deleted_user_email_never_send :string # en_construction_at :datetime # en_construction_close_to_expiration_notice_sent_at :datetime # en_construction_conservation_extension :interval default(0 seconds) @@ -137,9 +138,9 @@ class Dossier < ApplicationRecord end event :repasser_en_instruction, after: :after_repasser_en_instruction do - transitions from: :refuse, to: :en_instruction - transitions from: :sans_suite, to: :en_instruction - transitions from: :accepte, to: :en_instruction + transitions from: :refuse, to: :en_instruction, guard: :can_repasser_en_instruction? + transitions from: :sans_suite, to: :en_instruction, guard: :can_repasser_en_instruction? + transitions from: :accepte, to: :en_instruction, guard: :can_repasser_en_instruction? end end @@ -249,6 +250,7 @@ class Dossier < ApplicationRecord states = opts[:notify_on_closed] ? [:publiee, :close, :depubliee] : [:publiee, :depubliee] joins(:procedure) .where(procedures: { aasm_state: states }) + .where.not(user_id: nil) end scope :brouillon_close_to_expiration, -> do @@ -350,6 +352,22 @@ class Dossier < ApplicationRecord validates :individual, presence: true, if: -> { revision.procedure.for_individual? } validates :groupe_instructeur, presence: true, if: -> { !brouillon? } + def user_deleted? + user_id.nil? + end + + def user_email_for(use) + if user_deleted? + if use == :display + deleted_user_email_never_send + else + raise "Can not send email to discarded user" + end + else + user.email + end + end + def motivation return nil if !termine? traitements.any? ? traitements.last.motivation : read_attribute(:motivation) @@ -416,6 +434,10 @@ class Dossier < ApplicationRecord brouillon? && procedure.dossier_can_transition_to_en_construction? end + def can_repasser_en_instruction? + termine? && !user_deleted? + end + def can_be_updated_by_user? brouillon? || en_construction? end @@ -429,7 +451,7 @@ class Dossier < ApplicationRecord end def messagerie_available? - !brouillon? && !archived + !brouillon? && !user_deleted? && !archived end def en_construction_close_to_expiration? @@ -590,13 +612,18 @@ class Dossier < ApplicationRecord administration_emails.each do |email| DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later end - DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later + + if !user_deleted? + DossierMailer.notify_deletion_to_user(deleted_dossier, user_email_for(:notification)).deliver_later + end log_dossier_operation(author, :supprimer, self) elsif termine? deleted_dossier = DeletedDossier.create_from_dossier(self, reason) - DossierMailer.notify_instructeur_deletion_to_user(deleted_dossier, user.email).deliver_later + if !user_deleted? + DossierMailer.notify_instructeur_deletion_to_user(deleted_dossier, user_email_for(:notification)).deliver_later + end log_dossier_operation(author, :supprimer, self) end @@ -751,7 +778,7 @@ class Dossier < ApplicationRecord def spreadsheet_columns(with_etablissement: false, types_de_champ:, types_de_champ_private:) columns = [ ['ID', id.to_s], - ['Email', user.email] + ['Email', user_email_for(:display)] ] if procedure.for_individual? diff --git a/app/models/user.rb b/app/models/user.rb index f9f6fa372..1e40adb85 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -170,18 +170,24 @@ class User < ApplicationRecord end def can_be_deleted? - !administrateur? && !instructeur? && !expert? && dossiers.with_discarded.state_instruction_commencee.empty? + !administrateur? && !instructeur? && !expert? end def delete_and_keep_track_dossiers(administration) if !can_be_deleted? - raise "Cannot delete this user because instruction has started for some dossiers" + raise "Cannot delete this user because they are also instructeur, expert or administrateur" end - dossiers.each do |dossier| + Invite.where(dossier: dossiers.with_discarded).destroy_all + dossiers.state_en_construction.each do |dossier| dossier.discard_and_keep_track!(administration, :user_removed) end - dossiers.with_discarded.destroy_all + DossierOperationLog + .where(dossier: dossiers.with_discarded.discarded) + .where.not(operation: DossierOperationLog.operations.fetch(:supprimer)) + .destroy_all + dossiers.with_discarded.discarded.destroy_all + dossiers.update_all(deleted_user_email_never_send: email, user_id: nil) destroy! end diff --git a/app/views/dossiers/show.pdf.prawn b/app/views/dossiers/show.pdf.prawn index da28de24d..087502214 100644 --- a/app/views/dossiers/show.pdf.prawn +++ b/app/views/dossiers/show.pdf.prawn @@ -174,7 +174,7 @@ def add_message(pdf, message) if message.sent_by_system? sender = 'Email automatique' elsif message.sent_by?(@dossier.user) - sender = @dossier.user.email + sender = @dossier.user_email_for(:display) end format_in_2_lines(pdf, "#{sender}, #{try_format_date(message.created_at)}", @@ -233,7 +233,7 @@ prawn_document(page_size: "A4") do |pdf| format_in_2_columns(pdf, 'Informations France Connect', "Le dossier a été déposé par le compte de #{@dossier.france_connect_information.given_name} #{@dossier.france_connect_information.family_name}, authentifié par France Connect le #{@dossier.france_connect_information.updated_at.strftime('%d/%m/%Y')}") end - format_in_2_columns(pdf, "Email", @dossier.user.email) + format_in_2_columns(pdf, "Email", @dossier.user_email_for(:display)) if @dossier.individual.present? add_identite_individual(pdf, @dossier.individual) diff --git a/app/views/experts/avis/procedure.html.haml b/app/views/experts/avis/procedure.html.haml index fc79795d2..8abe0f3d4 100644 --- a/app/views/experts/avis/procedure.html.haml +++ b/app/views/experts/avis/procedure.html.haml @@ -38,7 +38,7 @@ = link_to(instructeur_avis_path(avis.procedure, avis), class: 'cell-link') do %span.icon.folder #{avis.dossier.id} - %td= link_to(avis.dossier.user.email, instructeur_avis_path(avis.procedure, avis), class: 'cell-link') + %td= link_to(avis.dossier.user_email_for(:display), instructeur_avis_path(avis.procedure, avis), class: 'cell-link') %td= link_to(avis.procedure.libelle, instructeur_avis_path(avis.procedure, avis), class: 'cell-link') = paginate(@avis) - else diff --git a/app/views/instructeurs/avis/procedure.html.haml b/app/views/instructeurs/avis/procedure.html.haml index f38d6cde3..1505eef58 100644 --- a/app/views/instructeurs/avis/procedure.html.haml +++ b/app/views/instructeurs/avis/procedure.html.haml @@ -38,7 +38,7 @@ = link_to(instructeur_avis_path(avis.procedure, avis), class: 'cell-link') do %span.icon.folder #{avis.dossier.id} - %td= link_to(avis.dossier.user.email, instructeur_avis_path(avis.procedure, avis), class: 'cell-link') + %td= link_to(avis.dossier.user_email_for(:display), instructeur_avis_path(avis.procedure, avis), class: 'cell-link') %td= link_to(avis.procedure.libelle, instructeur_avis_path(avis.procedure, avis), class: 'cell-link') = paginate(@avis) - else diff --git a/app/views/instructeurs/dossiers/_state_button.html.haml b/app/views/instructeurs/dossiers/_state_button.html.haml index 2b4d5851c..3d7b8e7bc 100644 --- a/app/views/instructeurs/dossiers/_state_button.html.haml +++ b/app/views/instructeurs/dossiers/_state_button.html.haml @@ -99,17 +99,24 @@ %h4 Voir l’attestation %p Cette attestation a été envoyée automatiquement au demandeur. - %li - = link_to repasser_en_instruction_instructeur_dossier_path(dossier.procedure, dossier), method: :post, data: { remote:true, confirm: "Voulez vous remettre le dossier #{dossier.id} en instruction ?" } do - %span.icon.in-progress - .dropdown-description - %h4 Repasser en instruction - L’usager sera notifié que son dossier est réexaminé. - - if dossier.termine? + - if dossier.can_repasser_en_instruction? %li - = link_to supprimer_dossier_instructeur_dossier_path(dossier.procedure, dossier), method: :patch, data: { confirm: "Voulez vous vraiment supprimer le dossier #{dossier.id} ? Cette action est irréversible. \nNous vous suggérons de télécharger le dossier au format PDF au préalable." } do - %span.icon.delete + = link_to repasser_en_instruction_instructeur_dossier_path(dossier.procedure, dossier), method: :post, data: { remote:true, confirm: "Voulez vous remettre le dossier #{dossier.id} en instruction ?" } do + %span.icon.in-progress .dropdown-description - %h4 Supprimer le dossier - L’usager sera notifié que son dossier est supprimé. + %h4 Repasser en instruction + L’usager sera notifié que son dossier est réexaminé. + - elsif dossier.user_deleted? + %li + %span.icon.info + .dropdown-description + %h4 En attente d‘archivage + L'usager a supprimé son compte. Vous pouvez archiver puis supprimer le dossier. + + %li + = link_to supprimer_dossier_instructeur_dossier_path(dossier.procedure, dossier), method: :patch, data: { confirm: "Voulez vous vraiment supprimer le dossier #{dossier.id} ? Cette action est irréversible. \nNous vous suggérons de télécharger le dossier au format PDF au préalable." } do + %span.icon.delete + .dropdown-description + %h4 Supprimer le dossier + L’usager sera notifié que son dossier est supprimé. diff --git a/app/views/instructeurs/recherche/index.html.haml b/app/views/instructeurs/recherche/index.html.haml index 7c7c3ce89..b08a849b5 100644 --- a/app/views/instructeurs/recherche/index.html.haml +++ b/app/views/instructeurs/recherche/index.html.haml @@ -27,7 +27,7 @@ = link_to(dossier_linked_path(current_instructeur, dossier), class: 'cell-link') do = dossier.id %td= link_to(dossier.procedure.libelle, dossier_linked_path(current_instructeur, dossier), class: 'cell-link') - %td= link_to(dossier.user.email, dossier_linked_path(current_instructeur, dossier), class: 'cell-link') + %td= link_to(dossier.user_email_for(:display), dossier_linked_path(current_instructeur, dossier), class: 'cell-link') %td.status-col = link_to(dossier_linked_path(current_instructeur, dossier), class: 'cell-link') do = status_badge(dossier.state) diff --git a/db/migrate/20210419100831_add_deleted_user_email_never_send_to_dossiers.rb b/db/migrate/20210419100831_add_deleted_user_email_never_send_to_dossiers.rb new file mode 100644 index 000000000..8238f3e9c --- /dev/null +++ b/db/migrate/20210419100831_add_deleted_user_email_never_send_to_dossiers.rb @@ -0,0 +1,5 @@ +class AddDeletedUserEmailNeverSendToDossiers < ActiveRecord::Migration[6.1] + def change + add_column :dossiers, :deleted_user_email_never_send, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 44108f050..09d1a2239 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: 2021_04_28_104228) do +ActiveRecord::Schema.define(version: 2021_04_28_114228) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -275,6 +275,7 @@ ActiveRecord::Schema.define(version: 2021_04_28_104228) do t.datetime "last_commentaire_updated_at" t.string "api_entreprise_job_exceptions", array: true t.interval "conservation_extension", default: "PT0S" + t.string "deleted_user_email_never_send" t.index "to_tsvector('french'::regconfig, (search_terms || private_search_terms))", name: "index_dossiers_on_search_terms_private_search_terms", using: :gin t.index "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin t.index ["archived"], name: "index_dossiers_on_archived" diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 459fbae5d..6ad1040b9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -263,13 +263,16 @@ describe User, type: :model do describe '#can_be_deleted?' do let(:user) { create(:user) } + let(:administrateur) { create(:administrateur) } + let(:instructeur) { create(:instructeur) } + let(:expert) { create(:expert) } subject { user.can_be_deleted? } context 'when the user has a dossier in instruction' do let!(:dossier) { create(:dossier, :en_instruction, user: user) } - it { is_expected.to be false } + it { is_expected.to be true } end context 'when the user has no dossier in instruction' do @@ -278,19 +281,19 @@ describe User, type: :model do context 'when the user is an administrateur' do it 'cannot be deleted' do - administrateur = create(:administrateur) - user = administrateur.user - - expect(user.can_be_deleted?).to be_falsy + expect(administrateur.user.can_be_deleted?).to be_falsy end end context 'when the user is an instructeur' do it 'cannot be deleted' do - instructeur = create(:instructeur) - user = instructeur.user + expect(instructeur.user.can_be_deleted?).to be_falsy + end + end - expect(user.can_be_deleted?).to be_falsy + context 'when the user is an expert' do + it 'cannot be deleted' do + expect(expert.user.can_be_deleted?).to be_falsy end end end @@ -299,14 +302,7 @@ describe User, type: :model do let(:super_admin) { create(:super_admin) } let(:user) { create(:user) } - context 'with a dossier in instruction' do - let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) } - it 'raises' do - expect { user.delete_and_keep_track_dossiers(super_admin) }.to raise_error(RuntimeError) - end - end - - context 'without a dossier in instruction' do + context 'without a dossier with processing strted' do let!(:dossier_en_construction) { create(:dossier, :en_construction, user: user) } let!(:dossier_brouillon) { create(:dossier, user: user) } @@ -321,28 +317,39 @@ describe User, type: :model do end context 'with a discarded dossier' do - let!(:dossier_cache) do - create(:dossier, :en_construction, user: user) - end - let!(:dossier_from_another_user) do - create(:dossier, :en_construction, user: create(:user)) - end + let(:dossier_to_discard) { create(:dossier, :en_construction, user: user) } + let!(:dossier_from_another_user) { create(:dossier, :en_construction, user: create(:user)) } it "keep track of dossiers and delete user" do - dossier_cache.discard_and_keep_track!(super_admin, :user_request) + dossier_to_discard.discard_and_keep_track!(super_admin, :user_request) user.delete_and_keep_track_dossiers(super_admin) expect(DeletedDossier.find_by(dossier_id: dossier_en_construction)).to be_present expect(DeletedDossier.find_by(dossier_id: dossier_brouillon)).to be_nil + expect(Dossier.find_by(id: dossier_from_another_user.id)).to be_present expect(User.find_by(id: user.id)).to be_nil end + end + end - it "doesn't destroy dossiers of another user" do - dossier_cache.discard_and_keep_track!(super_admin, :user_request) - user.delete_and_keep_track_dossiers(super_admin) + context 'with dossiers with processing strted' do + let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) } + let!(:dossier_termine) { create(:dossier, :accepte, user: user) } - expect(Dossier.find_by(id: dossier_from_another_user.id)).to be_present - end + it "keep track of dossiers and delete user" do + user.delete_and_keep_track_dossiers(super_admin) + + expect(dossier_en_instruction.reload).to be_present + expect(dossier_en_instruction.user).to be_nil + expect(dossier_en_instruction.user_email_for(:display)).to eq(user.email) + expect { dossier_en_instruction.user_email_for(:notification) }.to raise_error(RuntimeError) + + expect(dossier_termine.reload).to be_present + expect(dossier_termine.user).to be_nil + expect(dossier_termine.user_email_for(:display)).to eq(user.email) + expect { dossier_termine.user_email_for(:notification) }.to raise_error(RuntimeError) + + expect(User.find_by(id: user.id)).to be_nil end end end