demarches-normaliennes/app/models/dossier.rb
Pierre de La Morinerie a8354bd103 dossiers: unify deletion of dossiers between manager and user
The code paths for deleting a dossier were different, depending on
whether the dossier was deleted by the user, or from the Manager.

This commit unifies the two code paths into one.

This has the effect of:

- An operation log is now recorded when an user deletes its own dossier;
- Gestionnaires are now notified even when the dossier is deleted from
  the Manager;
- The `support:delete_user_account` task now requires the email address
  of the author.
2019-07-16 16:22:20 +02:00

517 lines
16 KiB
Ruby

class Dossier < ApplicationRecord
include DossierFilteringConcern
enum state: {
brouillon: 'brouillon',
en_construction: 'en_construction',
en_instruction: 'en_instruction',
accepte: 'accepte',
refuse: 'refuse',
sans_suite: 'sans_suite'
}
EN_CONSTRUCTION_OU_INSTRUCTION = [states.fetch(:en_construction), states.fetch(:en_instruction)]
TERMINE = [states.fetch(:accepte), states.fetch(:refuse), states.fetch(:sans_suite)]
INSTRUCTION_COMMENCEE = TERMINE + [states.fetch(:en_instruction)]
SOUMIS = EN_CONSTRUCTION_OU_INSTRUCTION + TERMINE
has_one :etablissement, dependent: :destroy
has_one :individual, dependent: :destroy
has_one :attestation, dependent: :destroy
has_many :pieces_justificatives, inverse_of: :dossier, dependent: :destroy
has_one_attached :justificatif_motivation
has_many :champs, -> { root.public_only.ordered }, inverse_of: :dossier, dependent: :destroy
has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', inverse_of: :dossier, dependent: :destroy
has_many :commentaires, inverse_of: :dossier, dependent: :destroy
has_many :invites, dependent: :destroy
has_many :follows, -> { active }, inverse_of: :dossier
has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
has_many :followers_gestionnaires, through: :follows, source: :gestionnaire
has_many :previous_followers_gestionnaires, -> { distinct }, through: :previous_follows, source: :gestionnaire
has_many :avis, inverse_of: :dossier, dependent: :destroy
has_many :dossier_operation_logs, dependent: :destroy
belongs_to :procedure
belongs_to :user
accepts_nested_attributes_for :champs
accepts_nested_attributes_for :champs_private
include AASM
aasm whiny_persistence: true, column: :state, enum: true do
state :brouillon, initial: true
state :en_construction
state :en_instruction
state :accepte
state :refuse
state :sans_suite
event :passer_en_construction, after: :after_passer_en_construction do
transitions from: :brouillon, to: :en_construction
end
event :passer_en_instruction, after: :after_passer_en_instruction do
transitions from: :en_construction, to: :en_instruction
end
event :passer_automatiquement_en_instruction, after: :after_passer_automatiquement_en_instruction do
transitions from: :en_construction, to: :en_instruction
end
event :repasser_en_construction, after: :after_repasser_en_construction do
transitions from: :en_instruction, to: :en_construction
end
event :accepter, after: :after_accepter do
transitions from: :en_instruction, to: :accepte
end
event :accepter_automatiquement, after: :after_accepter_automatiquement do
transitions from: :en_construction, to: :accepte
end
event :refuser, after: :after_refuser do
transitions from: :en_instruction, to: :refuse
end
event :classer_sans_suite, after: :after_classer_sans_suite do
transitions from: :en_instruction, to: :sans_suite
end
event :repasser_en_instruction, after: :after_repasser_en_instruction do
transitions from: :refuse, to: :en_instruction
transitions from: :sans_suite, to: :en_instruction
end
end
default_scope { where(hidden_at: nil) }
scope :state_brouillon, -> { where(state: states.fetch(:brouillon)) }
scope :state_not_brouillon, -> { where.not(state: states.fetch(:brouillon)) }
scope :state_en_construction, -> { where(state: states.fetch(:en_construction)) }
scope :state_en_instruction, -> { where(state: states.fetch(:en_instruction)) }
scope :state_en_construction_ou_instruction, -> { where(state: EN_CONSTRUCTION_OU_INSTRUCTION) }
scope :state_instruction_commencee, -> { where(state: INSTRUCTION_COMMENCEE) }
scope :state_termine, -> { where(state: TERMINE) }
scope :archived, -> { where(archived: true) }
scope :not_archived, -> { where(archived: false) }
scope :order_by_updated_at, -> (order = :desc) { order(updated_at: order) }
scope :order_for_api, -> (order = :asc) { order(en_construction_at: order, created_at: order, id: order) }
scope :all_state, -> { not_archived.state_not_brouillon }
scope :en_construction, -> { not_archived.state_en_construction }
scope :en_instruction, -> { not_archived.state_en_instruction }
scope :termine, -> { not_archived.state_termine }
scope :downloadable_sorted, -> { state_not_brouillon.includes(:etablissement, :user, :individual, :followers_gestionnaires, :avis, champs: { etablissement: [:champ], type_de_champ: :drop_down_list }, champs_private: { etablissement: [:champ], type_de_champ: :drop_down_list }).order(en_construction_at: 'asc') }
scope :en_cours, -> { not_archived.state_en_construction_ou_instruction }
scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) }
scope :followed_by, -> (gestionnaire) { joins(:follows).where(follows: { gestionnaire: gestionnaire }) }
scope :with_champs, -> { includes(champs: :type_de_champ) }
scope :nearing_end_of_retention, -> (duration = '1 month') { joins(:procedure).where("en_instruction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - now() < interval ?", duration) }
scope :since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
scope :for_api, -> {
includes(commentaires: { piece_jointe_attachment: :blob },
champs: [
:geo_areas,
:etablissement,
piece_justificative_file_attachment: :blob
],
champs_private: [
:geo_areas,
:etablissement,
piece_justificative_file_attachment: :blob
],
pieces_justificatives: [],
avis: [],
etablissement: [],
individual: [],
user: [])
}
accepts_nested_attributes_for :individual
delegate :siret, :siren, to: :etablissement, allow_nil: true
delegate :types_de_piece_justificative, to: :procedure
delegate :types_de_champ, to: :procedure
delegate :france_connect_information, to: :user
before_validation :update_state_dates, if: -> { state_changed? }
before_save :build_default_champs, if: Proc.new { procedure_id_changed? }
before_save :build_default_individual, if: Proc.new { procedure.for_individual? }
before_save :update_search_terms
after_save :send_dossier_received
after_save :send_web_hook
after_create :send_draft_notification_email
validates :user, presence: true
def update_search_terms
self.search_terms = [
user&.email,
*champs.flat_map(&:search_terms),
*etablissement&.search_terms,
individual&.nom,
individual&.prenom
].compact.join(' ')
self.private_search_terms = champs_private.flat_map(&:search_terms).compact.join(' ')
end
def was_piece_justificative_uploaded_for_type_id?(type_id)
pieces_justificatives.where(type_de_piece_justificative_id: type_id).count > 0
end
def retrieve_last_piece_justificative_by_type(type)
pieces_justificatives.where(type_de_piece_justificative_id: type).last
end
def retrieve_all_piece_justificative_by_type(type)
pieces_justificatives.where(type_de_piece_justificative_id: type).order(created_at: :DESC)
end
def build_default_champs
procedure.build_champs.each do |champ|
champs << champ
end
procedure.build_champs_private.each do |champ|
champs_private << champ
end
end
def build_default_individual
if Individual.where(dossier_id: self.id).count == 0
build_individual
end
end
def en_construction_ou_instruction?
EN_CONSTRUCTION_OU_INSTRUCTION.include?(state)
end
def termine?
TERMINE.include?(state)
end
def instruction_commencee?
INSTRUCTION_COMMENCEE.include?(state)
end
def reset!
etablissement.destroy
update_columns(autorisation_donnees: false)
end
def read_only?
en_instruction? || accepte? || refuse? || sans_suite?
end
def can_transition_to_en_construction?
!procedure.archivee? && brouillon?
end
def can_be_updated_by_user?
brouillon? || en_construction?
end
def can_be_deleted_by_user?
brouillon? || en_construction?
end
def messagerie_available?
!brouillon? && !archived
end
def retention_end_date
if instruction_commencee?
en_instruction_at + procedure.duree_conservation_dossiers_dans_ds.months
end
end
def retention_expired?
instruction_commencee? && retention_end_date <= Time.zone.now
end
def text_summary
if brouillon?
parts = [
"Dossier en brouillon répondant à la démarche ",
procedure.libelle,
" gérée par l'organisme ",
procedure.organisation_name
]
else
parts = [
"Dossier déposé le ",
en_construction_at.strftime("%d/%m/%Y"),
" sur la démarche ",
procedure.libelle,
" gérée par l'organisme ",
procedure.organisation_name
]
end
parts.join
end
def avis_for(gestionnaire)
if gestionnaire.dossiers.include?(self)
avis.order(created_at: :asc)
else
avis
.where(confidentiel: false)
.or(avis.where(claimant: gestionnaire))
.or(avis.where(gestionnaire: gestionnaire))
.order(created_at: :asc)
end
end
def owner_name
if etablissement.present?
etablissement.entreprise_raison_sociale
elsif individual.present?
"#{individual.nom} #{individual.prenom}"
end
end
def expose_legacy_carto_api?
procedure.expose_legacy_carto_api?
end
def geo_position
if etablissement.present?
point = ApiAdresse::PointAdapter.new(etablissement.geo_adresse).geocode
end
lon = "2.428462"
lat = "46.538192"
zoom = "13"
if point.present?
lon = point.x.to_s
lat = point.y.to_s
end
{ lon: lon, lat: lat, zoom: zoom }
end
def unspecified_attestation_champs
attestation_template = procedure.attestation_template
if attestation_template&.activated?
attestation_template.unspecified_champs_for_dossier(self)
else
[]
end
end
def build_attestation
if procedure.attestation_template&.activated?
procedure.attestation_template.attestation_for(self)
end
end
def delete_and_keep_track(author)
deleted_dossier = DeletedDossier.create_from_dossier(self)
update(hidden_at: deleted_dossier.deleted_at)
if en_construction?
administration_emails = followers_gestionnaires.present? ? followers_gestionnaires.pluck(:email) : procedure.administrateurs.pluck(:email)
administration_emails.each do |email|
DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later
end
end
DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later
log_dossier_operation(author, :supprimer, self)
end
def after_passer_en_instruction(gestionnaire)
gestionnaire.follow(self)
log_dossier_operation(gestionnaire, :passer_en_instruction)
end
def after_passer_automatiquement_en_instruction
log_automatic_dossier_operation(:passer_en_instruction)
end
def after_repasser_en_construction(gestionnaire)
self.en_instruction_at = nil
save!
log_dossier_operation(gestionnaire, :repasser_en_construction)
end
def after_repasser_en_instruction(gestionnaire)
self.processed_at = nil
self.motivation = nil
attestation&.destroy
save!
DossierMailer.notify_revert_to_instruction(self).deliver_later
log_dossier_operation(gestionnaire, :repasser_en_instruction)
end
def after_accepter(gestionnaire, motivation, justificatif = nil)
self.motivation = motivation
if justificatif
self.justificatif_motivation.attach(justificatif)
end
if attestation.nil?
self.attestation = build_attestation
end
save!
NotificationMailer.send_closed_notification(self).deliver_later
log_dossier_operation(gestionnaire, :accepter, self)
end
def after_accepter_automatiquement
self.en_instruction_at ||= Time.zone.now
if attestation.nil?
self.attestation = build_attestation
end
save!
NotificationMailer.send_closed_notification(self).deliver_later
log_automatic_dossier_operation(:accepter, self)
end
def after_refuser(gestionnaire, motivation, justificatif = nil)
self.motivation = motivation
if justificatif
self.justificatif_motivation.attach(justificatif)
end
save!
NotificationMailer.send_refused_notification(self).deliver_later
log_dossier_operation(gestionnaire, :refuser, self)
end
def after_classer_sans_suite(gestionnaire, motivation, justificatif = nil)
self.motivation = motivation
if justificatif
self.justificatif_motivation.attach(justificatif)
end
save!
NotificationMailer.send_without_continuation_notification(self).deliver_later
log_dossier_operation(gestionnaire, :classer_sans_suite, self)
end
def check_mandatory_champs
(champs + champs.select(&:repetition?).flat_map(&:champs))
.select(&:mandatory_and_blank?)
.map do |champ|
"Le champ #{champ.libelle.truncate(200)} doit être rempli."
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
def spreadsheet_columns
[
['ID', id.to_s],
['Email', user.email],
['Civilité', individual&.gender],
['Nom', individual&.nom],
['Prénom', individual&.prenom],
['Date de naissance', individual&.birthdate],
['Archivé', :archived],
['État du dossier', I18n.t(state, scope: [:activerecord, :attributes, :dossier, :state])],
['Dernière mise à jour le', :updated_at],
['Passé en construction le', :en_construction_at],
['Passé en instruction le', :en_instruction_at],
['Traité le', :processed_at],
['Motivation de la décision', :motivation],
['Instructeurs', followers_gestionnaires.map(&:email).join(' ')]
] + champs_for_export + annotations_for_export
end
def champs_for_export
champs.reject(&:exclude_from_export?).map do |champ|
[champ.libelle, champ.for_export]
end
end
def annotations_for_export
champs_private.reject(&:exclude_from_export?).map do |champ|
[champ.libelle, champ.for_export]
end
end
def attachments_downloadable?
!PiecesJustificativesService.liste_pieces_justificatives(self).empty? && PiecesJustificativesService.pieces_justificatives_total_size(self) < 50.megabytes
end
private
def log_dossier_operation(author, operation, subject = nil)
DossierOperationLog.create_and_serialize(
dossier: self,
operation: DossierOperationLog.operations.fetch(operation),
author: author,
subject: subject
)
end
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
def update_state_dates
if en_construction? && !self.en_construction_at
self.en_construction_at = Time.zone.now
elsif en_instruction? && !self.en_instruction_at
self.en_instruction_at = Time.zone.now
elsif TERMINE.include?(state)
self.processed_at = Time.zone.now
end
end
def send_dossier_received
if saved_change_to_state? && en_instruction? && !procedure.declarative_accepte?
NotificationMailer.send_dossier_received(self).deliver_later
end
end
def send_draft_notification_email
if brouillon? && !procedure.declarative?
DossierMailer.notify_new_draft(self).deliver_later
end
end
def send_web_hook
if saved_change_to_state? && !brouillon? && procedure.web_hook_url
WebHookJob.perform_later(
procedure,
self
)
end
end
end