diff --git a/app/controllers/admin/procedures_controller.rb b/app/controllers/admin/procedures_controller.rb index cd6259131..a9cde8461 100644 --- a/app/controllers/admin/procedures_controller.rb +++ b/app/controllers/admin/procedures_controller.rb @@ -52,15 +52,14 @@ class Admin::ProceduresController < AdminController def destroy procedure = current_administrateur.procedures.find(params[:id]) - if procedure.locked? - return render json: {}, status: 401 + if procedure.can_be_deleted_by_administrateur? + procedure.discard_and_keep_track!(current_administrateur) + + flash.notice = 'Démarche supprimée' + redirect_to admin_procedures_draft_path + else + render json: {}, status: 403 end - - procedure.reset! - procedure.destroy - - flash.notice = 'Démarche supprimée' - redirect_to admin_procedures_draft_path end def publish_validate diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 4fce78b3f..7d02e0069 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -116,7 +116,7 @@ module Instructeurs def deleted_dossiers @procedure = procedure @deleted_dossiers = @procedure - .deleted_dossiers.where.not(state: :brouillon) + .deleted_dossiers .order(:dossier_id) .page params[:page] end diff --git a/app/controllers/manager/dossiers_controller.rb b/app/controllers/manager/dossiers_controller.rb index 143dbf048..ebf98f7d1 100644 --- a/app/controllers/manager/dossiers_controller.rb +++ b/app/controllers/manager/dossiers_controller.rb @@ -30,6 +30,15 @@ module Manager redirect_to manager_dossier_path(dossier) end + def restore + dossier = Dossier.with_discarded.find(params[:id]) + dossier.restore(current_administration) + + flash[:notice] = "Le dossier #{dossier.id} a été restauré." + + redirect_to manager_dossier_path(dossier) + end + def repasser_en_instruction dossier = Dossier.find(params[:id]) dossier.repasser_en_instruction(current_administration) diff --git a/app/controllers/manager/procedures_controller.rb b/app/controllers/manager/procedures_controller.rb index 0d05bed76..a6c45dbd9 100644 --- a/app/controllers/manager/procedures_controller.rb +++ b/app/controllers/manager/procedures_controller.rb @@ -8,7 +8,7 @@ module Manager # this will be used to set the records shown on the `index` action. def scoped_resource if unfiltered_list? - # Don't display deleted dossiers in the unfiltered list… + # Don't display discarded demarches in the unfiltered list… Procedure.kept else # … but allow them to be searched and displayed. @@ -22,10 +22,21 @@ module Manager redirect_to manager_procedure_path(procedure) end - def hide - procedure.hide! - flash[:notice] = "La démarche a bien été supprimée, en cas d'erreur contactez un développeur." - redirect_to manager_procedures_path + def discard + procedure.discard_and_keep_track!(current_administration) + + logger.info("La démarche #{procedure.id} est supprimée par #{current_administration.email}") + flash[:notice] = "La démarche #{procedure.id} a été supprimée." + + redirect_to manager_procedure_path(procedure) + end + + def restore + procedure.restore(current_administration) + + flash[:notice] = "La démarche #{procedure.id} a été restauré." + + redirect_to manager_procedure_path(procedure) end def add_administrateur @@ -51,7 +62,7 @@ module Manager private def procedure - Procedure.find(params[:id]) + @procedure ||= Procedure.with_discarded.find(params[:id]) end def type_de_champ diff --git a/app/controllers/new_administrateur/groupe_instructeurs_controller.rb b/app/controllers/new_administrateur/groupe_instructeurs_controller.rb index 8732b2bb1..b07b844b6 100644 --- a/app/controllers/new_administrateur/groupe_instructeurs_controller.rb +++ b/app/controllers/new_administrateur/groupe_instructeurs_controller.rb @@ -71,7 +71,7 @@ module NewAdministrateur def reaffecter target_group = procedure.groupe_instructeurs.find(params[:target_group]) - groupe_instructeur.dossiers.find_each do |dossier| + groupe_instructeur.dossiers.with_discarded.find_each do |dossier| dossier.assign_to_groupe_instructeur(target_group, current_administrateur) end diff --git a/app/jobs/discarded_procedures_deletion_job.rb b/app/jobs/discarded_procedures_deletion_job.rb new file mode 100644 index 000000000..8ea745e89 --- /dev/null +++ b/app/jobs/discarded_procedures_deletion_job.rb @@ -0,0 +1,10 @@ +class DiscardedProceduresDeletionJob < CronJob + self.cron_expression = "0 7 * * *" + + def perform(*args) + Procedure.discarded_expired.find_each do |procedure| + procedure.dossiers.with_discarded.destroy_all + procedure.destroy + end + end +end diff --git a/app/mailers/dossier_mailer.rb b/app/mailers/dossier_mailer.rb index 6bc61df26..5c7dde111 100644 --- a/app/mailers/dossier_mailer.rb +++ b/app/mailers/dossier_mailer.rb @@ -70,6 +70,7 @@ class DossierMailer < ApplicationMailer end def notify_automatic_deletion_to_user(deleted_dossiers, to_email) + @state = deleted_dossiers.first.state @subject = default_i18n_subject(count: deleted_dossiers.count) @deleted_dossiers = deleted_dossiers @@ -83,15 +84,17 @@ class DossierMailer < ApplicationMailer mail(to: to_email, subject: @subject) end - def notify_en_construction_near_deletion_to_user(dossiers, to_email) - @subject = default_i18n_subject(count: dossiers.count) + def notify_near_deletion_to_user(dossiers, to_email) + @state = dossiers.first.state + @subject = default_i18n_subject(count: dossiers.count, state: @state) @dossiers = dossiers mail(to: to_email, subject: @subject) end - def notify_en_construction_near_deletion_to_administration(dossiers, to_email) - @subject = default_i18n_subject(count: dossiers.count) + def notify_near_deletion_to_administration(dossiers, to_email) + @state = dossiers.first.state + @subject = default_i18n_subject(count: dossiers.count, state: @state) @dossiers = dossiers mail(to: to_email, subject: @subject) @@ -111,4 +114,17 @@ class DossierMailer < ApplicationMailer mail(to: dossier.user.email, subject: @subject) end + + protected + + # This is an override of `default_i18n_subject` method + # https://api.rubyonrails.org/v5.0.0/classes/ActionMailer/Base.html#method-i-default_i18n_subject + def default_i18n_subject(interpolations = {}) + if interpolations[:state] + mailer_scope = self.class.mailer_name.tr('/', '.') + I18n.t("subject_#{interpolations[:state]}", interpolations.merge(scope: [mailer_scope, action_name])) + else + super + end + end end diff --git a/app/models/deleted_dossier.rb b/app/models/deleted_dossier.rb index bce5518ee..30317bbf8 100644 --- a/app/models/deleted_dossier.rb +++ b/app/models/deleted_dossier.rb @@ -1,11 +1,14 @@ class DeletedDossier < ApplicationRecord - belongs_to :procedure + belongs_to :procedure, -> { with_discarded }, inverse_of: :deleted_dossiers + + validates :dossier_id, uniqueness: true enum reason: { - user_request: 'user_request', - manager_request: 'manager_request', - user_removed: 'user_removed', - expired: 'expired' + user_request: 'user_request', + manager_request: 'manager_request', + user_removed: 'user_removed', + procedure_removed: 'procedure_removed', + expired: 'expired' } def self.create_from_dossier(dossier, reason) @@ -17,4 +20,8 @@ class DeletedDossier < ApplicationRecord deleted_at: Time.zone.now ) end + + def procedure_removed? + reason == self.class.reasons.fetch(:procedure_removed) + end end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 544acfd20..c8e13c658 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -212,9 +212,7 @@ class Dossier < ApplicationRecord with_discarded .discarded .state_en_construction - .joins(:procedure) .where('dossiers.hidden_at < ?', 1.month.ago) - .where(procedures: { hidden_at: nil }) end scope :brouillon_near_procedure_closing_date, -> do @@ -487,6 +485,19 @@ class Dossier < ApplicationRecord discard! end + def restore(author, only_discarded_with_procedure = false) + if discarded? + deleted_dossier = DeletedDossier.find_by(dossier_id: id) + + if !only_discarded_with_procedure || deleted_dossier&.procedure_removed? + if undiscard && keep_track_on_deletion? && en_construction? + deleted_dossier&.destroy + log_dossier_operation(author, :restaurer, self) + end + end + end + end + def after_passer_en_instruction(instructeur) instructeur.follow(self) @@ -748,7 +759,9 @@ class Dossier < ApplicationRecord followers_instructeurs.each do |instructeur| if instructeur.groupe_instructeurs.exclude?(groupe_instructeur) instructeur.unfollow(self) - DossierMailer.notify_groupe_instructeur_changed(instructeur, self).deliver_later + if kept? + DossierMailer.notify_groupe_instructeur_changed(instructeur, self).deliver_later + end end end end diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index f1cd8ce1e..924fbb962 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -8,6 +8,7 @@ class DossierOperationLog < ApplicationRecord refuser: 'refuser', classer_sans_suite: 'classer_sans_suite', supprimer: 'supprimer', + restaurer: 'restaurer', modifier_annotation: 'modifier_annotation', demander_un_avis: 'demander_un_avis' } diff --git a/app/models/groupe_instructeur.rb b/app/models/groupe_instructeur.rb index ccb84e5f1..ca883e2a5 100644 --- a/app/models/groupe_instructeur.rb +++ b/app/models/groupe_instructeur.rb @@ -1,6 +1,6 @@ class GroupeInstructeur < ApplicationRecord DEFAULT_LABEL = 'défaut' - belongs_to :procedure + belongs_to :procedure, -> { with_discarded }, inverse_of: :groupe_instructeurs has_many :assign_tos has_many :instructeurs, through: :assign_tos, dependent: :destroy has_many :dossiers diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 3c3a72fca..f21b5fbb8 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -53,6 +53,12 @@ class Procedure < ApplicationRecord scope :cloned_from_library, -> { where(cloned_from_library: true) } scope :declarative, -> { where.not(declarative_with_state: nil) } + scope :discarded_expired, -> do + with_discarded + .discarded + .where('hidden_at < ?', 1.month.ago) + end + scope :for_api, -> { includes( :administrateurs, @@ -511,9 +517,34 @@ class Procedure < ApplicationRecord groupe_instructeurs.count > 1 end - def hide! + def can_be_deleted_by_administrateur? + brouillon? || dossiers.state_instruction_commencee.empty? + end + + def can_be_deleted_by_manager? + kept? && can_be_deleted_by_administrateur? + end + + def discard_and_keep_track!(author) + if brouillon? + reset! + elsif publiee? + close! + end + + dossiers.each do |dossier| + dossier.discard_and_keep_track!(author, :procedure_removed) + end + discard! - dossiers.discard_all + end + + def restore(author) + if discarded? && undiscard + dossiers.with_discarded.discarded.find_each do |dossier| + dossier.restore(author, true) + end + end end def flipper_id diff --git a/app/policies/champ_policy.rb b/app/policies/champ_policy.rb index 81d2eb11b..a55efe1a0 100644 --- a/app/policies/champ_policy.rb +++ b/app/policies/champ_policy.rb @@ -5,16 +5,30 @@ class ChampPolicy < ApplicationPolicy return scope.none end + # The join must be the same for all elements of the WHERE clause. + # + # NB: here we want to do `.left_outer_joins(dossier: [:invites, { :groupe_instructeur: :instructeurs }]))`, + # but for some reasons ActiveRecord <= 5.2 generates bogus SQL. Hence the manual version of it below. + joined_scope = scope + .joins('LEFT OUTER JOIN dossiers ON dossiers.id = champs.dossier_id') + .joins('LEFT OUTER JOIN invites ON invites.dossier_id = dossiers.id') + .joins('LEFT OUTER JOIN groupe_instructeurs ON groupe_instructeurs.id = dossiers.groupe_instructeur_id') + .joins('LEFT OUTER JOIN assign_tos ON assign_tos.groupe_instructeur_id = groupe_instructeurs.id') + .joins('LEFT OUTER JOIN instructeurs ON instructeurs.id = assign_tos.instructeur_id') + # Users can access public champs on their own dossiers. - resolved_scope = scope - .left_outer_joins(dossier: { groupe_instructeur: [:instructeurs] }) + resolved_scope = joined_scope .where('dossiers.user_id': user.id, private: false) + # Invited users can access public champs on dossiers they are invited to + invite_clause = joined_scope + .where('invites.user_id': user.id, private: false) + resolved_scope = resolved_scope.or(invite_clause) + if instructeur.present? # Additionnaly, instructeurs can access private champs # on dossiers they are allowed to instruct. - instructeur_clause = scope - .left_outer_joins(dossier: { groupe_instructeur: [:instructeurs] }) + instructeur_clause = joined_scope .where('instructeurs.id': instructeur.id, private: true) resolved_scope = resolved_scope.or(instructeur_clause) end diff --git a/app/services/expired_dossiers_deletion_service.rb b/app/services/expired_dossiers_deletion_service.rb index e47fff4e2..9b8efaa29 100644 --- a/app/services/expired_dossiers_deletion_service.rb +++ b/app/services/expired_dossiers_deletion_service.rb @@ -33,23 +33,7 @@ class ExpiredDossiersDeletionService .en_construction_close_to_expiration .without_en_construction_expiration_notice_sent - dossiers_close_to_expiration - .with_notifiable_procedure - .includes(:user) - .group_by(&:user) - .each do |(user, dossiers)| - DossierMailer.notify_en_construction_near_deletion_to_user( - dossiers, - user.email - ).deliver_later - end - - group_by_fonctionnaire_email(dossiers_close_to_expiration).each do |(email, dossiers)| - DossierMailer.notify_en_construction_near_deletion_to_administration( - dossiers, - email - ).deliver_later - end + send_expiration_notices(dossiers_close_to_expiration) dossiers_close_to_expiration.update_all(en_construction_close_to_expiration_notice_sent_at: Time.zone.now) end @@ -72,7 +56,32 @@ class ExpiredDossiersDeletionService end def self.delete_expired_en_construction_and_notify - dossiers_to_remove = Dossier.en_construction_expired + delete_expired_and_notify(Dossier.en_construction_expired) + end + + private + + def self.send_expiration_notices(dossiers_close_to_expiration) + dossiers_close_to_expiration + .with_notifiable_procedure + .includes(:user) + .group_by(&:user) + .each do |(user, dossiers)| + DossierMailer.notify_near_deletion_to_user( + dossiers, + user.email + ).deliver_later + end + + group_by_fonctionnaire_email(dossiers_close_to_expiration).each do |(email, dossiers)| + DossierMailer.notify_near_deletion_to_administration( + dossiers.to_a, + email + ).deliver_later + end + end + + def self.delete_expired_and_notify(dossiers_to_remove) dossiers_to_remove.each(&:expired_keep_track!) dossiers_to_remove @@ -96,8 +105,6 @@ class ExpiredDossiersDeletionService dossiers_to_remove.destroy_all end - private - def self.group_by_fonctionnaire_email(dossiers) dossiers .with_notifiable_procedure diff --git a/app/views/admin/procedures/_list.html.haml b/app/views/admin/procedures/_list.html.haml index 1828c02ff..33165d4ec 100644 --- a/app/views/admin/procedures/_list.html.haml +++ b/app/views/admin/procedures/_list.html.haml @@ -25,7 +25,7 @@ %td= link_to(try_format_datetime(procedure.created_at), admin_procedure_href) %td = link_to('Cloner', admin_procedure_clone_path(procedure.id), data: { method: :put }, class: 'btn-sm btn-primary clone-btn') - - if !procedure.locked? + - if !procedure.can_be_deleted_by_administrateur? = link_to('X', url_for(controller: 'admin/procedures', action: :destroy, id: procedure.id), data: { method: :delete, confirm: "Confirmez-vous la suppression de la démarche ? \n\n Attention : toute suppression est définitive et s’appliquera aux éventuels autres administrateurs de cette démarche !" }, class: 'btn-sm btn-danger') = smart_listing.paginate diff --git a/app/views/dossier_mailer/notify_automatic_deletion_to_user.html.haml b/app/views/dossier_mailer/notify_automatic_deletion_to_user.html.haml index 8a824675c..bf35cabe2 100644 --- a/app/views/dossier_mailer/notify_automatic_deletion_to_user.html.haml +++ b/app/views/dossier_mailer/notify_automatic_deletion_to_user.html.haml @@ -9,6 +9,7 @@ - @deleted_dossiers.each do |d| %li n° #{d.dossier_id} (#{d.procedure.libelle}) -%p= t('.footer', count: @deleted_dossiers.count) +- if @state == Dossier.states.fetch(:en_construction) + %p= t('.footer_en_construction', count: @deleted_dossiers.count) = render partial: "layouts/mailers/signature" diff --git a/app/views/dossier_mailer/notify_en_construction_near_deletion_to_administration.html.haml b/app/views/dossier_mailer/notify_near_deletion_to_administration.html.haml similarity index 64% rename from app/views/dossier_mailer/notify_en_construction_near_deletion_to_administration.html.haml rename to app/views/dossier_mailer/notify_near_deletion_to_administration.html.haml index b89f885f9..62b0156ff 100644 --- a/app/views/dossier_mailer/notify_en_construction_near_deletion_to_administration.html.haml +++ b/app/views/dossier_mailer/notify_near_deletion_to_administration.html.haml @@ -4,13 +4,14 @@ Bonjour, %p - = t('.header', count: @dossiers.count) + = t('.header_en_construction', count: @dossiers.count) %ul - @dossiers.each do |d| %li #{link_to("n° #{d.id} (#{d.procedure.libelle})", dossier_url(d))}. Retrouvez le dossier au format #{link_to("PDF", instructeur_dossier_url(d.procedure, d, format: :pdf))} %p - = sanitize(t('.footer', count: @dossiers.count)) + - if @state == Dossier.states.fetch(:en_construction) + = sanitize(t('.footer_en_construction', count: @dossiers.count)) = render partial: "layouts/mailers/signature" diff --git a/app/views/dossier_mailer/notify_en_construction_near_deletion_to_user.html.haml b/app/views/dossier_mailer/notify_near_deletion_to_user.html.haml similarity index 86% rename from app/views/dossier_mailer/notify_en_construction_near_deletion_to_user.html.haml rename to app/views/dossier_mailer/notify_near_deletion_to_user.html.haml index 30dd62ad6..efe0c668d 100644 --- a/app/views/dossier_mailer/notify_en_construction_near_deletion_to_user.html.haml +++ b/app/views/dossier_mailer/notify_near_deletion_to_user.html.haml @@ -4,7 +4,7 @@ Bonjour, %p - = t('.header', count: @dossiers.count) + = t('.header_en_construction', count: @dossiers.count) %ul - @dossiers.each do |d| %li diff --git a/app/views/manager/dossiers/show.html.erb b/app/views/manager/dossiers/show.html.erb index 140497f4c..12b1415cc 100644 --- a/app/views/manager/dossiers/show.html.erb +++ b/app/views/manager/dossiers/show.html.erb @@ -33,6 +33,8 @@ as well as a link to its edit page. <% end %> <% if dossier.can_be_deleted_by_manager? %> <%= link_to 'Supprimer le dossier', discard_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Confirmez vous la suppression du dossier ?" } %> + <% elsif dossier.discarded? && !dossier.procedure.discarded? %> + <%= link_to 'Restaurer le dossier', restore_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Confirmez vous la restauration du dossier ?" } %> <% end %> diff --git a/app/views/manager/procedures/show.html.erb b/app/views/manager/procedures/show.html.erb index dd020bb5f..faa058874 100644 --- a/app/views/manager/procedures/show.html.erb +++ b/app/views/manager/procedures/show.html.erb @@ -22,6 +22,9 @@ as well as a link to its edit page.