Enable user destruction

This commit is contained in:
Paul Chavard 2021-05-01 12:20:24 +02:00
parent e240dd6d2c
commit a4fd629f4a
14 changed files with 126 additions and 63 deletions

View file

@ -41,7 +41,7 @@ module Manager
def export_mail_brouillons def export_mail_brouillons
dossiers = procedure.dossiers.state_brouillon.includes(:user) 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') date = Time.zone.now.strftime('%d-%m-%Y')
send_data(emails.join("\n"), :filename => "brouillons-#{procedure.id}-au-#{date}.csv") send_data(emails.join("\n"), :filename => "brouillons-#{procedure.id}-au-#{date}.csv")
end end

View file

@ -54,8 +54,12 @@ module Types
end end
def usager def usager
if object.user_deleted?
{ email: object.user_email_for(:display), id: -1 }
else
Loaders::Record.for(User).load(object.user_id) Loaders::Record.for(User).load(object.user_id)
end end
end
def groupe_instructeur def groupe_instructeur
Loaders::Record.for(GroupeInstructeur).load(object.groupe_instructeur_id) Loaders::Record.for(GroupeInstructeur).load(object.groupe_instructeur_id)

View file

@ -13,7 +13,7 @@ class DossierMailer < ApplicationMailer
subject = "Retrouvez votre brouillon pour la démarche « #{dossier.procedure.libelle} »" 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' } format.html { render layout: 'mailers/notifications_layout' }
end end
end end
@ -25,7 +25,7 @@ class DossierMailer < ApplicationMailer
subject = "Nouveau message pour votre dossier nº #{dossier.id} (#{dossier.procedure.libelle})" 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' } format.html { render layout: 'mailers/notifications_layout' }
end end
end end
@ -49,7 +49,7 @@ class DossierMailer < ApplicationMailer
subject = "Votre dossier nº #{@dossier.id} est en train d'être réexaminé" 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' } format.html { render layout: 'mailers/notifications_layout' }
end end
end end
@ -139,7 +139,7 @@ class DossierMailer < ApplicationMailer
@subject = "Attention : votre dossier n'est pas déposé." @subject = "Attention : votre dossier n'est pas déposé."
@dossier = dossier @dossier = dossier
mail(to: dossier.user.email, subject: @subject) mail(to: dossier.user_email_for(:notification), subject: @subject)
end end
protected protected

View file

@ -8,6 +8,8 @@
class NotificationMailer < ApplicationMailer class NotificationMailer < ApplicationMailer
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
before_action :prevent_delivery_to_deleted_users
helper ServiceHelper helper ServiceHelper
helper MailerHelper helper MailerHelper
@ -36,8 +38,12 @@ class NotificationMailer < ApplicationMailer
private private
def prevent_delivery_to_deleted_users
!@dossier.user_deleted?
end
def send_notification(dossier, mail_template) def send_notification(dossier, mail_template)
email = dossier.user.email email = dossier.user_email_for(:notification)
subject = mail_template.subject_for_dossier(dossier) subject = mail_template.subject_for_dossier(dossier)
body = mail_template.body_for_dossier(dossier) body = mail_template.body_for_dossier(dossier)

View file

@ -8,6 +8,7 @@
# autorisation_donnees :boolean # autorisation_donnees :boolean
# brouillon_close_to_expiration_notice_sent_at :datetime # brouillon_close_to_expiration_notice_sent_at :datetime
# conservation_extension :interval default(0 seconds) # conservation_extension :interval default(0 seconds)
# deleted_user_email_never_send :string
# en_construction_at :datetime # en_construction_at :datetime
# en_construction_close_to_expiration_notice_sent_at :datetime # en_construction_close_to_expiration_notice_sent_at :datetime
# en_construction_conservation_extension :interval default(0 seconds) # en_construction_conservation_extension :interval default(0 seconds)
@ -137,9 +138,9 @@ class Dossier < ApplicationRecord
end end
event :repasser_en_instruction, after: :after_repasser_en_instruction do event :repasser_en_instruction, after: :after_repasser_en_instruction do
transitions from: :refuse, to: :en_instruction transitions from: :refuse, to: :en_instruction, guard: :can_repasser_en_instruction?
transitions from: :sans_suite, to: :en_instruction transitions from: :sans_suite, to: :en_instruction, guard: :can_repasser_en_instruction?
transitions from: :accepte, to: :en_instruction transitions from: :accepte, to: :en_instruction, guard: :can_repasser_en_instruction?
end end
end end
@ -249,6 +250,7 @@ class Dossier < ApplicationRecord
states = opts[:notify_on_closed] ? [:publiee, :close, :depubliee] : [:publiee, :depubliee] states = opts[:notify_on_closed] ? [:publiee, :close, :depubliee] : [:publiee, :depubliee]
joins(:procedure) joins(:procedure)
.where(procedures: { aasm_state: states }) .where(procedures: { aasm_state: states })
.where.not(user_id: nil)
end end
scope :brouillon_close_to_expiration, -> do scope :brouillon_close_to_expiration, -> do
@ -350,6 +352,22 @@ class Dossier < ApplicationRecord
validates :individual, presence: true, if: -> { revision.procedure.for_individual? } validates :individual, presence: true, if: -> { revision.procedure.for_individual? }
validates :groupe_instructeur, presence: true, if: -> { !brouillon? } 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 def motivation
return nil if !termine? return nil if !termine?
traitements.any? ? traitements.last.motivation : read_attribute(:motivation) traitements.any? ? traitements.last.motivation : read_attribute(:motivation)
@ -416,6 +434,10 @@ class Dossier < ApplicationRecord
brouillon? && procedure.dossier_can_transition_to_en_construction? brouillon? && procedure.dossier_can_transition_to_en_construction?
end end
def can_repasser_en_instruction?
termine? && !user_deleted?
end
def can_be_updated_by_user? def can_be_updated_by_user?
brouillon? || en_construction? brouillon? || en_construction?
end end
@ -429,7 +451,7 @@ class Dossier < ApplicationRecord
end end
def messagerie_available? def messagerie_available?
!brouillon? && !archived !brouillon? && !user_deleted? && !archived
end end
def en_construction_close_to_expiration? def en_construction_close_to_expiration?
@ -590,13 +612,18 @@ class Dossier < ApplicationRecord
administration_emails.each do |email| administration_emails.each do |email|
DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later
end 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) log_dossier_operation(author, :supprimer, self)
elsif termine? elsif termine?
deleted_dossier = DeletedDossier.create_from_dossier(self, reason) 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) log_dossier_operation(author, :supprimer, self)
end end
@ -751,7 +778,7 @@ class Dossier < ApplicationRecord
def spreadsheet_columns(with_etablissement: false, types_de_champ:, types_de_champ_private:) def spreadsheet_columns(with_etablissement: false, types_de_champ:, types_de_champ_private:)
columns = [ columns = [
['ID', id.to_s], ['ID', id.to_s],
['Email', user.email] ['Email', user_email_for(:display)]
] ]
if procedure.for_individual? if procedure.for_individual?

View file

@ -170,18 +170,24 @@ class User < ApplicationRecord
end end
def can_be_deleted? def can_be_deleted?
!administrateur? && !instructeur? && !expert? && dossiers.with_discarded.state_instruction_commencee.empty? !administrateur? && !instructeur? && !expert?
end end
def delete_and_keep_track_dossiers(administration) def delete_and_keep_track_dossiers(administration)
if !can_be_deleted? 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 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) dossier.discard_and_keep_track!(administration, :user_removed)
end 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! destroy!
end end

View file

@ -174,7 +174,7 @@ def add_message(pdf, message)
if message.sent_by_system? if message.sent_by_system?
sender = 'Email automatique' sender = 'Email automatique'
elsif message.sent_by?(@dossier.user) elsif message.sent_by?(@dossier.user)
sender = @dossier.user.email sender = @dossier.user_email_for(:display)
end end
format_in_2_lines(pdf, "#{sender}, #{try_format_date(message.created_at)}", 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')}") 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 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? if @dossier.individual.present?
add_identite_individual(pdf, @dossier.individual) add_identite_individual(pdf, @dossier.individual)

View file

@ -38,7 +38,7 @@
= link_to(instructeur_avis_path(avis.procedure, avis), class: 'cell-link') do = link_to(instructeur_avis_path(avis.procedure, avis), class: 'cell-link') do
%span.icon.folder %span.icon.folder
#{avis.dossier.id} #{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') %td= link_to(avis.procedure.libelle, instructeur_avis_path(avis.procedure, avis), class: 'cell-link')
= paginate(@avis) = paginate(@avis)
- else - else

View file

@ -38,7 +38,7 @@
= link_to(instructeur_avis_path(avis.procedure, avis), class: 'cell-link') do = link_to(instructeur_avis_path(avis.procedure, avis), class: 'cell-link') do
%span.icon.folder %span.icon.folder
#{avis.dossier.id} #{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') %td= link_to(avis.procedure.libelle, instructeur_avis_path(avis.procedure, avis), class: 'cell-link')
= paginate(@avis) = paginate(@avis)
- else - else

View file

@ -99,13 +99,20 @@
%h4 Voir lattestation %h4 Voir lattestation
%p Cette attestation a été envoyée automatiquement au demandeur. %p Cette attestation a été envoyée automatiquement au demandeur.
- if dossier.can_repasser_en_instruction?
%li %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 = 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 %span.icon.in-progress
.dropdown-description .dropdown-description
%h4 Repasser en instruction %h4 Repasser en instruction
Lusager sera notifié que son dossier est réexaminé. Lusager sera notifié que son dossier est réexaminé.
- if dossier.termine? - elsif dossier.user_deleted?
%li
%span.icon.info
.dropdown-description
%h4 En attente darchivage
L'usager a supprimé son compte. Vous pouvez archiver puis supprimer le dossier.
%li %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 = 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 %span.icon.delete

View file

@ -27,7 +27,7 @@
= link_to(dossier_linked_path(current_instructeur, dossier), class: 'cell-link') do = link_to(dossier_linked_path(current_instructeur, dossier), class: 'cell-link') do
= dossier.id = dossier.id
%td= link_to(dossier.procedure.libelle, dossier_linked_path(current_instructeur, dossier), class: 'cell-link') %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 %td.status-col
= link_to(dossier_linked_path(current_instructeur, dossier), class: 'cell-link') do = link_to(dossier_linked_path(current_instructeur, dossier), class: 'cell-link') do
= status_badge(dossier.state) = status_badge(dossier.state)

View file

@ -0,0 +1,5 @@
class AddDeletedUserEmailNeverSendToDossiers < ActiveRecord::Migration[6.1]
def change
add_column :dossiers, :deleted_user_email_never_send, :string
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -275,6 +275,7 @@ ActiveRecord::Schema.define(version: 2021_04_28_104228) do
t.datetime "last_commentaire_updated_at" t.datetime "last_commentaire_updated_at"
t.string "api_entreprise_job_exceptions", array: true t.string "api_entreprise_job_exceptions", array: true
t.interval "conservation_extension", default: "PT0S" 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 || 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 "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin
t.index ["archived"], name: "index_dossiers_on_archived" t.index ["archived"], name: "index_dossiers_on_archived"

View file

@ -263,13 +263,16 @@ describe User, type: :model do
describe '#can_be_deleted?' do describe '#can_be_deleted?' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:administrateur) { create(:administrateur) }
let(:instructeur) { create(:instructeur) }
let(:expert) { create(:expert) }
subject { user.can_be_deleted? } subject { user.can_be_deleted? }
context 'when the user has a dossier in instruction' do context 'when the user has a dossier in instruction' do
let!(:dossier) { create(:dossier, :en_instruction, user: user) } let!(:dossier) { create(:dossier, :en_instruction, user: user) }
it { is_expected.to be false } it { is_expected.to be true }
end end
context 'when the user has no dossier in instruction' do 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 context 'when the user is an administrateur' do
it 'cannot be deleted' do it 'cannot be deleted' do
administrateur = create(:administrateur) expect(administrateur.user.can_be_deleted?).to be_falsy
user = administrateur.user
expect(user.can_be_deleted?).to be_falsy
end end
end end
context 'when the user is an instructeur' do context 'when the user is an instructeur' do
it 'cannot be deleted' do it 'cannot be deleted' do
instructeur = create(:instructeur) expect(instructeur.user.can_be_deleted?).to be_falsy
user = instructeur.user 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 end
end end
@ -299,14 +302,7 @@ describe User, type: :model do
let(:super_admin) { create(:super_admin) } let(:super_admin) { create(:super_admin) }
let(:user) { create(:user) } let(:user) { create(:user) }
context 'with a dossier in instruction' do context 'without a dossier with processing strted' 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
let!(:dossier_en_construction) { create(:dossier, :en_construction, user: user) } let!(:dossier_en_construction) { create(:dossier, :en_construction, user: user) }
let!(:dossier_brouillon) { create(:dossier, user: user) } let!(:dossier_brouillon) { create(:dossier, user: user) }
@ -321,28 +317,39 @@ describe User, type: :model do
end end
context 'with a discarded dossier' do context 'with a discarded dossier' do
let!(:dossier_cache) do let(:dossier_to_discard) { create(:dossier, :en_construction, user: user) }
create(:dossier, :en_construction, user: user) let!(:dossier_from_another_user) { create(:dossier, :en_construction, user: create(:user)) }
end
let!(:dossier_from_another_user) do
create(:dossier, :en_construction, user: create(:user))
end
it "keep track of dossiers and delete user" do 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) 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_en_construction)).to be_present
expect(DeletedDossier.find_by(dossier_id: dossier_brouillon)).to be_nil 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 expect(User.find_by(id: user.id)).to be_nil
end end
end
end
it "doesn't destroy dossiers of another user" do context 'with dossiers with processing strted' do
dossier_cache.discard_and_keep_track!(super_admin, :user_request) let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) }
let!(:dossier_termine) { create(:dossier, :accepte, user: user) }
it "keep track of dossiers and delete user" do
user.delete_and_keep_track_dossiers(super_admin) user.delete_and_keep_track_dossiers(super_admin)
expect(Dossier.find_by(id: dossier_from_another_user.id)).to be_present expect(dossier_en_instruction.reload).to be_present
end 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 end
end end