diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb
index 85bb998bf..00ccfefdc 100644
--- a/app/controllers/instructeurs/dossiers_controller.rb
+++ b/app/controllers/instructeurs/dossiers_controller.rb
@@ -214,6 +214,17 @@ module Instructeurs
zipline(files, "dossier-#{dossier.id}.zip")
end
+ def delete_dossier
+ if dossier.termine?
+ dossier.discard_and_keep_track!(current_instructeur, :instructeur_request)
+ flash.notice = 'Le dossier a bien été supprimé'
+ redirect_to instructeur_procedure_path(procedure)
+ else
+ flash.alert = "Suppression impossible : le dossier n'est pas terminé"
+ redirect_back(fallback_location: instructeur_procedures_url)
+ end
+ end
+
private
def dossier
diff --git a/app/controllers/super_admins/passwords_controller.rb b/app/controllers/super_admins/passwords_controller.rb
index acfcf6157..69c8de2d3 100644
--- a/app/controllers/super_admins/passwords_controller.rb
+++ b/app/controllers/super_admins/passwords_controller.rb
@@ -3,4 +3,17 @@ class SuperAdmins::PasswordsController < Devise::PasswordsController
super
self.resource.disable_otp!
end
+
+ def test_strength
+ @score, @words, @length = ZxcvbnService.new(password_params[:password]).complexity
+ @min_length = PASSWORD_MIN_LENGTH
+ @min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
+ render 'shared/password/test_strength'
+ end
+
+ private
+
+ def password_params
+ params.require(:super_admin).permit(:password)
+ end
end
diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb
index 362657b35..86da6303f 100644
--- a/app/controllers/users/dossiers_controller.rb
+++ b/app/controllers/users/dossiers_controller.rb
@@ -18,15 +18,8 @@ module Users
def index
@user_dossiers = current_user.dossiers.includes(:procedure).order_by_updated_at.page(page)
@dossiers_invites = current_user.dossiers_invites.includes(:procedure).order_by_updated_at.page(page)
-
- @current_tab = current_tab(@user_dossiers.count, @dossiers_invites.count)
-
- @dossiers = case @current_tab
- when 'mes-dossiers'
- @user_dossiers
- when 'dossiers-invites'
- @dossiers_invites
- end
+ @dossiers_supprimes = current_user.deleted_dossiers.order_by_updated_at.page(page)
+ @statut = statut(@user_dossiers, @dossiers_invites, @dossiers_supprimes, params[:statut])
end
def show
@@ -282,6 +275,25 @@ module Users
private
+ # if the status tab is filled, then this tab
+ # else first filled tab
+ # else mes-dossiers
+ def statut(mes_dossiers, dossiers_invites, dossiers_supprimes, params_statut)
+ tabs = {
+ 'mes-dossiers' => mes_dossiers.present?,
+ 'dossiers-invites' => dossiers_invites.present?,
+ 'dossiers-supprimes' => dossiers_supprimes.present?
+ }
+ if tabs[params_statut]
+ params_statut
+ else
+ tabs
+ .filter { |_tab, filled| filled }
+ .map { |tab, _| tab }
+ .first || 'mes-dossiers'
+ end
+ end
+
def store_user_location!
store_location_for(:user, request.fullpath)
end
@@ -292,7 +304,7 @@ module Users
def show_demarche_en_test_banner
if @dossier.present? && @dossier.procedure.brouillon?
- flash.now.alert = "Ce dossier est déposé sur une démarche en test. Toute modification de la démarche par l'administrateur (ajout d'un champ, publication de la démarche...) entrainera sa suppression."
+ flash.now.alert = "Ce dossier est déposé sur une démarche en test. Toute modification de la démarche par l'administrateur (ajout d'un champ, publication de la démarche...) entraînera sa suppression."
end
end
@@ -307,16 +319,6 @@ module Users
[params[:page].to_i, 1].max
end
- def current_tab(mes_dossiers_count, dossiers_invites_count)
- if dossiers_invites_count == 0
- 'mes-dossiers'
- elsif mes_dossiers_count == 0
- 'dossiers-invites'
- else
- params[:current_tab].presence || 'mes-dossiers'
- end
- end
-
# FIXME: require(:dossier) when all the champs are united
def champs_params
params.permit(dossier: {
diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql
index 0fe8b1878..81b5acf78 100644
--- a/app/graphql/schema.graphql
+++ b/app/graphql/schema.graphql
@@ -1055,13 +1055,13 @@ type PersonneMorale implements Demandeur {
association: Association
codeInseeLocalite: String!
codePostal: String!
- complementAdresse: String!
+ complementAdresse: String
entreprise: Entreprise
id: ID!
libelleNaf: String!
localite: String!
naf: String!
- nomVoie: String!
+ nomVoie: String
numeroVoie: String
siegeSocial: Boolean!
siret: String!
diff --git a/app/graphql/types/personne_morale_type.rb b/app/graphql/types/personne_morale_type.rb
index a0216046b..2a74a0cea 100644
--- a/app/graphql/types/personne_morale_type.rb
+++ b/app/graphql/types/personne_morale_type.rb
@@ -89,8 +89,8 @@ module Types
field :adresse, String, null: false
field :numero_voie, String, null: true
field :type_voie, String, null: true
- field :nom_voie, String, null: false
- field :complement_adresse, String, null: false
+ field :nom_voie, String, null: true
+ field :complement_adresse, String, null: true
field :code_postal, String, null: false
field :localite, String, null: false
field :code_insee_localite, String, null: false
diff --git a/app/jobs/cron/discarded_dossiers_deletion_job.rb b/app/jobs/cron/discarded_dossiers_deletion_job.rb
index 2a8d4b748..45dd82fe6 100644
--- a/app/jobs/cron/discarded_dossiers_deletion_job.rb
+++ b/app/jobs/cron/discarded_dossiers_deletion_job.rb
@@ -2,7 +2,15 @@ class Cron::DiscardedDossiersDeletionJob < Cron::CronJob
self.schedule_expression = "every day at 2 am"
def perform(*args)
+ DossierOperationLog.where(dossier: Dossier.discarded_en_construction_expired)
+ .where.not(operation: DossierOperationLog.operations.fetch(:supprimer))
+ .destroy_all
+ DossierOperationLog.where(dossier: Dossier.discarded_termine_expired)
+ .where.not(operation: DossierOperationLog.operations.fetch(:supprimer))
+ .destroy_all
+
Dossier.discarded_brouillon_expired.destroy_all
Dossier.discarded_en_construction_expired.destroy_all
+ Dossier.discarded_termine_expired.destroy_all
end
end
diff --git a/app/lib/active_storage/downloadable_file.rb b/app/lib/active_storage/downloadable_file.rb
index 914ca478d..92b86784f 100644
--- a/app/lib/active_storage/downloadable_file.rb
+++ b/app/lib/active_storage/downloadable_file.rb
@@ -23,15 +23,29 @@ class ActiveStorage::DownloadableFile
private
- def self.timestamped_filename(piece_justificative)
+ def self.timestamped_filename(attachment)
# we pad the original file name with a timestamp
# and a short id in order to help identify multiple versions and avoid name collisions
- extension = File.extname(piece_justificative.filename.to_s)
- basename = File.basename(piece_justificative.filename.to_s, extension)
- timestamp = piece_justificative.created_at.strftime("%d-%m-%Y-%H-%M")
- id = piece_justificative.id % 10000
+ folder = self.folder(attachment)
+ extension = File.extname(attachment.filename.to_s)
+ basename = File.basename(attachment.filename.to_s, extension)
+ timestamp = attachment.created_at.strftime("%d-%m-%Y-%H-%M")
+ id = attachment.id % 10000
- "#{basename}-#{timestamp}-#{id}#{extension}"
+ "#{folder}/#{basename}-#{timestamp}-#{id}#{extension}"
+ end
+
+ def self.folder(attachment)
+ case attachment.record_type
+ when 'Dossier'
+ 'dossier'
+ when 'DossierOperationLog', 'BillSignature'
+ 'horodatage'
+ when 'Commentaire'
+ 'messagerie'
+ else
+ 'pieces_justificatives'
+ end
end
def using_local_backend?
diff --git a/app/lib/helpscout/form_adapter.rb b/app/lib/helpscout/form_adapter.rb
index 6e4dcdaac..4b073953c 100644
--- a/app/lib/helpscout/form_adapter.rb
+++ b/app/lib/helpscout/form_adapter.rb
@@ -13,9 +13,9 @@ class Helpscout::FormAdapter
def self.admin_options
[
- [I18n.t(ADMIN_TYPE_QUESTION, scope: [:supportadmin]), ADMIN_TYPE_QUESTION],
- [I18n.t(ADMIN_TYPE_RDV, scope: [:supportadmin]), ADMIN_TYPE_RDV],
- [I18n.t(ADMIN_TYPE_SOUCIS, scope: [:supportadmin]), ADMIN_TYPE_SOUCIS],
+ [I18n.t(ADMIN_TYPE_QUESTION, scope: [:supportadmin], app_name: APPLICATION_NAME), ADMIN_TYPE_QUESTION],
+ [I18n.t(ADMIN_TYPE_RDV, scope: [:supportadmin], app_name: APPLICATION_NAME), ADMIN_TYPE_RDV],
+ [I18n.t(ADMIN_TYPE_SOUCIS, scope: [:supportadmin], app_name: APPLICATION_NAME), ADMIN_TYPE_SOUCIS],
[I18n.t(ADMIN_TYPE_PRODUIT, scope: [:supportadmin]), ADMIN_TYPE_PRODUIT],
[I18n.t(ADMIN_TYPE_DEMANDE_COMPTE, scope: [:supportadmin]), ADMIN_TYPE_DEMANDE_COMPTE],
[I18n.t(ADMIN_TYPE_AUTRE, scope: [:supportadmin]), ADMIN_TYPE_AUTRE]
diff --git a/app/mailers/dossier_mailer.rb b/app/mailers/dossier_mailer.rb
index c650c0a8f..1d340c32b 100644
--- a/app/mailers/dossier_mailer.rb
+++ b/app/mailers/dossier_mailer.rb
@@ -75,6 +75,20 @@ class DossierMailer < ApplicationMailer
mail(to: to_email, subject: @subject)
end
+ def notify_instructeur_deletion_to_user(deleted_dossier, to_email)
+ @subject = default_i18n_subject(libelle_demarche: deleted_dossier.procedure.libelle)
+ @deleted_dossier = deleted_dossier
+
+ mail(to: to_email, subject: @subject)
+ end
+
+ def notify_instructeur(deleted_dossier, to_email)
+ @subject = default_i18n_subject(dossier_id: deleted_dossier.dossier_id)
+ @deleted_dossier = deleted_dossier
+
+ mail(to: to_email, subject: @subject)
+ end
+
def notify_deletion_to_administration(deleted_dossier, to_email)
@subject = default_i18n_subject(dossier_id: deleted_dossier.dossier_id)
@deleted_dossier = deleted_dossier
diff --git a/app/models/deleted_dossier.rb b/app/models/deleted_dossier.rb
index 98f69d8a2..1d465fef9 100644
--- a/app/models/deleted_dossier.rb
+++ b/app/models/deleted_dossier.rb
@@ -19,12 +19,15 @@ class DeletedDossier < ApplicationRecord
validates :dossier_id, uniqueness: true
+ scope :order_by_updated_at, -> (order = :desc) { order(created_at: order) }
+
enum reason: {
user_request: 'user_request',
manager_request: 'manager_request',
user_removed: 'user_removed',
procedure_removed: 'procedure_removed',
- expired: 'expired'
+ expired: 'expired',
+ instructeur_request: 'instructeur_request'
}
def self.create_from_dossier(dossier, reason)
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index 3766f0cf1..e9bc7da9b 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -263,13 +263,19 @@ class Dossier < ApplicationRecord
with_discarded
.discarded
.state_brouillon
- .where('hidden_at < ?', 1.month.ago)
+ .where('hidden_at < ?', 1.week.ago)
end
scope :discarded_en_construction_expired, -> do
with_discarded
.discarded
.state_en_construction
- .where('dossiers.hidden_at < ?', 1.month.ago)
+ .where('dossiers.hidden_at < ?', 1.week.ago)
+ end
+ scope :discarded_termine_expired, -> do
+ with_discarded
+ .discarded
+ .state_termine
+ .where('dossiers.hidden_at < ?', 1.week.ago)
end
scope :brouillon_near_procedure_closing_date, -> do
@@ -521,16 +527,24 @@ class Dossier < ApplicationRecord
end
def discard_and_keep_track!(author, reason)
- if keep_track_on_deletion? && en_construction?
- deleted_dossier = DeletedDossier.create_from_dossier(self, reason)
+ if keep_track_on_deletion?
+ if en_construction?
+ deleted_dossier = DeletedDossier.create_from_dossier(self, reason)
- administration_emails = followers_instructeurs.present? ? followers_instructeurs.map(&:email) : procedure.administrateurs.map(&:email)
- administration_emails.each do |email|
- DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later
+ administration_emails = followers_instructeurs.present? ? followers_instructeurs.map(&:email) : procedure.administrateurs.map(&:email)
+ 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
+
+ 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
+
+ log_dossier_operation(author, :supprimer, self)
end
- DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later
-
- log_dossier_operation(author, :supprimer, self)
end
discard!
diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb
index f52254f31..846dbadda 100644
--- a/app/models/dossier_operation_log.rb
+++ b/app/models/dossier_operation_log.rb
@@ -58,7 +58,7 @@ class DossierOperationLog < ApplicationRecord
operation: operation_log.operation,
dossier_id: operation_log.dossier_id,
author: self.serialize_author(params[:author]),
- subject: self.serialize_subject(params[:subject]),
+ subject: self.serialize_subject(params[:subject], operation_log.operation),
automatic_operation: operation_log.automatic_operation?,
executed_at: operation_log.executed_at.iso8601
}.compact.to_json
@@ -84,9 +84,15 @@ class DossierOperationLog < ApplicationRecord
end
end
- def self.serialize_subject(subject)
+ def self.serialize_subject(subject, operation = nil)
if subject.nil?
nil
+ elsif operation == operations.fetch(:supprimer)
+ {
+ date_de_depot: subject.en_construction_at,
+ date_de_mise_en_instruction: subject.en_instruction_at,
+ date_de_decision: subject.termine? ? subject.traitements.last.processed_at : nil
+ }.as_json
else
case subject
when Dossier
diff --git a/app/models/super_admin.rb b/app/models/super_admin.rb
index a10c45052..eeb69a255 100644
--- a/app/models/super_admin.rb
+++ b/app/models/super_admin.rb
@@ -28,6 +28,8 @@ class SuperAdmin < ApplicationRecord
devise :rememberable, :trackable, :validatable, :lockable, :async, :recoverable,
:two_factor_authenticatable, :otp_secret_encryption_key => Rails.application.secrets.otp_secret_key
+ validates :password, password_complexity: true, if: -> (u) { Devise.password_length.include?(u.password.try(:size)) }
+
def enable_otp!
self.otp_secret = SuperAdmin.generate_otp_secret
self.otp_required_for_login = true
diff --git a/app/models/user.rb b/app/models/user.rb
index 1582b6a6c..d1356face 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -44,6 +44,7 @@ class User < ApplicationRecord
has_many :invites, dependent: :destroy
has_many :dossiers_invites, through: :invites, source: :dossier
has_many :feedbacks, dependent: :destroy
+ has_many :deleted_dossiers
has_one :france_connect_information, dependent: :destroy
belongs_to :instructeur, optional: true
belongs_to :administrateur, optional: true
@@ -54,13 +55,7 @@ class User < ApplicationRecord
before_validation -> { sanitize_email(:email) }
- validate :password_complexity, if: -> (u) { u.administrateur.present? && Devise.password_length.include?(u.password.try(:size)) }
-
- def password_complexity
- if password.present? && ZxcvbnService.new(password).score < PASSWORD_COMPLEXITY_FOR_ADMIN
- errors.add(:password, :not_strong)
- end
- end
+ validates :password, password_complexity: true, if: -> (u) { u.administrateur.present? && Devise.password_length.include?(u.password.try(:size)) }
# Override of Devise::Models::Confirmable#send_confirmation_instructions
def send_confirmation_instructions
diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb
index 2c4e6fe80..a5903531b 100644
--- a/app/services/pieces_justificatives_service.rb
+++ b/app/services/pieces_justificatives_service.rb
@@ -2,8 +2,9 @@ class PiecesJustificativesService
def self.liste_pieces_justificatives(dossier)
pjs_champs = pjs_for_champs(dossier)
pjs_commentaires = pjs_for_commentaires(dossier)
+ pjs_dossier = pjs_for_dossier(dossier)
- (pjs_champs + pjs_commentaires)
+ (pjs_champs + pjs_commentaires + pjs_dossier)
.filter(&:attached?)
end
@@ -59,4 +60,18 @@ class PiecesJustificativesService
.commentaires
.map(&:piece_jointe)
end
+
+ def self.pjs_for_dossier(dossier)
+ bill_signatures = dossier.dossier_operation_logs.map(&:bill_signature).compact.uniq
+
+ [
+ dossier.justificatif_motivation,
+ dossier.attestation&.pdf,
+ dossier.etablissement&.entreprise_attestation_sociale,
+ dossier.etablissement&.entreprise_attestation_fiscale,
+ dossier.dossier_operation_logs.map(&:serialized),
+ bill_signatures.map(&:serialized),
+ bill_signatures.map(&:signature)
+ ].flatten.compact
+ end
end
diff --git a/app/validators/password_complexity_validator.rb b/app/validators/password_complexity_validator.rb
new file mode 100644
index 000000000..a915a8575
--- /dev/null
+++ b/app/validators/password_complexity_validator.rb
@@ -0,0 +1,7 @@
+class PasswordComplexityValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if value.present? && ZxcvbnService.new(value).score < PASSWORD_COMPLEXITY_FOR_ADMIN
+ record.errors.add(attribute, :not_strong)
+ end
+ end
+end
diff --git a/app/views/dossier_mailer/notify_instructeur_deletion_to_user.html.haml b/app/views/dossier_mailer/notify_instructeur_deletion_to_user.html.haml
new file mode 100644
index 000000000..0dde82206
--- /dev/null
+++ b/app/views/dossier_mailer/notify_instructeur_deletion_to_user.html.haml
@@ -0,0 +1,9 @@
+- content_for(:title, "#{@subject}")
+
+%p
+ Bonjour,
+
+%p
+ = t('.body_html', dossier_id: @deleted_dossier.dossier_id, libelle_demarche: @deleted_dossier.procedure.libelle, deleted_dossiers_link: dossiers_url(statut: 'dossiers-supprimes'))
+
+= render partial: "layouts/mailers/signature"
diff --git a/app/views/instructeurs/dossiers/_state_button.html.haml b/app/views/instructeurs/dossiers/_state_button.html.haml
index c6b58fcae..2b4d5851c 100644
--- a/app/views/instructeurs/dossiers/_state_button.html.haml
+++ b/app/views/instructeurs/dossiers/_state_button.html.haml
@@ -105,3 +105,11 @@
.dropdown-description
%h4 Repasser en instruction
L’usager sera notifié que son dossier est réexaminé.
+ - if dossier.termine?
+ %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/procedures/deleted_dossiers.html.haml b/app/views/instructeurs/procedures/deleted_dossiers.html.haml
index 65a2413a6..c9f0944dd 100644
--- a/app/views/instructeurs/procedures/deleted_dossiers.html.haml
+++ b/app/views/instructeurs/procedures/deleted_dossiers.html.haml
@@ -52,7 +52,6 @@
%tr
%th.notification-col
%th.number-col N° dossier
- %th.status-col Etat
%th.status-col Raison de suppression
%th.status-col Date de suppression
%tbody
@@ -62,8 +61,6 @@
%span.icon.folder
%td.number-col
= deleted_dossier.dossier_id
- %td.status-col
- = status_badge(deleted_dossier.state)
%td.reason-col
= deletion_reason_badge(deleted_dossier.reason)
%td.date-col.deleted-cell
diff --git a/app/views/super_admins/passwords/edit.html.haml b/app/views/super_admins/passwords/edit.html.haml
index 33b8d466b..d14e4ca7b 100644
--- a/app/views/super_admins/passwords/edit.html.haml
+++ b/app/views/super_admins/passwords/edit.html.haml
@@ -14,9 +14,8 @@
= f.hidden_field :reset_password_token
= f.label 'Nouveau mot de passe'
- = f.password_field :password, autofocus: true, autocomplete: 'off'
- = f.label 'Confirmez le nouveau mot de passe'
- = f.password_field :password_confirmation, autocomplete: 'off'
+ = render partial: 'shared/password/edit_password', locals: { form: f, controller: 'super_admins/passwords' }
- = f.submit 'Changer le mot de passe', class: 'button primary'
+
+ = f.submit 'Changer le mot de passe', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi..." }
diff --git a/app/views/users/dossiers/_deleted_dossiers_list.html.haml b/app/views/users/dossiers/_deleted_dossiers_list.html.haml
new file mode 100644
index 000000000..2b4efe2b3
--- /dev/null
+++ b/app/views/users/dossiers/_deleted_dossiers_list.html.haml
@@ -0,0 +1,43 @@
+- if deleted_dossiers.present?
+ %table.table.dossiers-table.hoverable
+ %thead
+ %tr
+ %th.number-col Nº dossier
+ %th Démarche
+ %th Raison de suppression
+ %th Date de suppression
+ %tbody
+ - deleted_dossiers.each do |dossier|
+ - libelle_demarche = Procedure.find(dossier.procedure_id).libelle
+ %tr{ data: { 'dossier-id': dossier.dossier_id } }
+ %td.number-col
+ %span.icon.folder
+ = dossier.dossier_id
+ %td
+ = libelle_demarche
+
+ %td.cell-link
+ = deletion_reason_badge(dossier.reason)
+ %td
+ = dossier.updated_at.strftime('%d/%m/%Y')
+
+ = paginate(deleted_dossiers)
+
+ - if current_user.feedbacks.empty? || current_user.feedbacks.last.created_at < 1.month.ago
+ #user-satisfaction
+ %h3 Que pensez-vous de la facilité d'utilisation de ce service ?
+ .icons
+ = link_to feedback_path(rating: Feedback.ratings.fetch(:unhappy)), data: { remote: true, method: :post } do
+ %span.icon.frown
+ = link_to feedback_path(rating: Feedback.ratings.fetch(:neutral)), data: { remote: true, method: :post } do
+ %span.icon.meh
+ = link_to feedback_path(rating: Feedback.ratings.fetch(:happy)), data: { remote: true, method: :post } do
+ %span.icon.smile
+
+- else
+ .blank-tab
+ %h2.empty-text Aucun dossier.
+ %p.empty-text-details
+ Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche.
+ %br
+ Celui ci doit ressembler à #{APPLICATION_BASE_URL}/commencer/xxx.
diff --git a/app/views/users/dossiers/_dossier_actions.html.haml b/app/views/users/dossiers/_dossier_actions.html.haml
index 1ae1a87f6..fd48bb53d 100644
--- a/app/views/users/dossiers/_dossier_actions.html.haml
+++ b/app/views/users/dossiers/_dossier_actions.html.haml
@@ -32,7 +32,7 @@
- if has_delete_action
%li.danger
- = link_to ask_deletion_dossier_path(dossier), method: :post, data: { disable: true, confirm: "En continuant, vous allez supprimer ce dossier ainsi que les informations qu’il contient. Toute suppression entraine l’annulation de la démarche en cours.\n\nConfirmer la suppression ?" } do
+ = link_to ask_deletion_dossier_path(dossier), method: :post, data: { disable: true, confirm: "En continuant, vous allez supprimer ce dossier ainsi que les informations qu’il contient. Toute suppression entraîne l’annulation de la démarche en cours.\n\nConfirmer la suppression ?" } do
%span.icon.delete
.dropdown-description
Supprimer le dossier
diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml
new file mode 100644
index 000000000..35a90aae3
--- /dev/null
+++ b/app/views/users/dossiers/_dossiers_list.html.haml
@@ -0,0 +1,51 @@
+- if dossiers.present?
+ %table.table.dossiers-table.hoverable
+ %thead
+ %tr
+ %th.number-col Nº dossier
+ %th Démarche
+ - if dossiers.present?
+ %th Demandeur
+ %th.status-col Statut
+ %th.updated-at-col Mis à jour
+ %th.sr-only Actions
+ %tbody
+ - dossiers.each do |dossier|
+ %tr{ data: { 'dossier-id': dossier.id } }
+ %td.number-col
+ = link_to(url_for_dossier(dossier), class: 'cell-link', tabindex: -1) do
+ %span.icon.folder
+ = dossier.id
+ %td
+ = link_to(url_for_dossier(dossier), class: 'cell-link') do
+ = procedure_libelle(dossier.procedure)
+ - if dossiers.present?
+ %td.cell-link
+ = demandeur_dossier(dossier)
+ %td.status-col
+ = status_badge(dossier.state)
+ %td.updated-at-col.cell-link
+ = try_format_date(dossier.updated_at)
+ %td.action-col
+ = render partial: 'dossier_actions', locals: { dossier: dossier }
+
+ = paginate(dossiers)
+
+ - if current_user.feedbacks.empty? || current_user.feedbacks.last.created_at < 1.month.ago
+ #user-satisfaction
+ %h3 Que pensez-vous de la facilité d'utilisation de ce service ?
+ .icons
+ = link_to feedback_path(rating: Feedback.ratings.fetch(:unhappy)), data: { remote: true, method: :post } do
+ %span.icon.frown
+ = link_to feedback_path(rating: Feedback.ratings.fetch(:neutral)), data: { remote: true, method: :post } do
+ %span.icon.meh
+ = link_to feedback_path(rating: Feedback.ratings.fetch(:happy)), data: { remote: true, method: :post } do
+ %span.icon.smile
+
+- else
+ .blank-tab
+ %h2.empty-text Aucun dossier.
+ %p.empty-text-details
+ Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche.
+ %br
+ Celui ci doit ressembler à #{APPLICATION_BASE_URL}/commencer/xxx.
diff --git a/app/views/users/dossiers/index.html.haml b/app/views/users/dossiers/index.html.haml
index a38137a03..28a9ae6f3 100644
--- a/app/views/users/dossiers/index.html.haml
+++ b/app/views/users/dossiers/index.html.haml
@@ -10,68 +10,36 @@
.container
- if @search_terms.present?
%h1.page-title Résultat de la recherche pour « #{@search_terms} »
- - elsif @dossiers_invites.count == 0
- %h1.page-title Mes dossiers
+ = render partial: "dossiers_list", locals: { dossiers: @dossiers }
- else
%h1.page-title Dossiers
%ul.tabs
- = tab_item('mes dossiers',
- dossiers_path(current_tab: 'mes-dossiers'),
- active: @current_tab == 'mes-dossiers')
+ - if @user_dossiers.count > 0
+ = tab_item(t('pluralize.mes_dossiers', count: @user_dossiers.count),
+ dossiers_path(statut: 'mes-dossiers'),
+ active: @statut == 'mes-dossiers',
+ badge: number_with_html_delimiter(@user_dossiers.count))
+
+ - if @dossiers_invites.count > 0
+ = tab_item(t('pluralize.dossiers_invites', count: @dossiers_invites.count),
+ dossiers_path(statut: 'dossiers-invites'),
+ active: @statut == 'dossiers-invites',
+ badge: number_with_html_delimiter(@dossiers_invites.count))
+
+ - if @dossiers_supprimes.count > 0
+ = tab_item(t('pluralize.dossiers_supprimes', count: @dossiers_supprimes.count),
+ dossiers_path(statut: 'dossiers-supprimes'),
+ active: @statut == 'dossiers-supprimes',
+ badge: number_with_html_delimiter(@dossiers_supprimes.count))
- = tab_item('dossiers invités',
- dossiers_path(current_tab: 'dossiers-invites'),
- active: @current_tab == 'dossiers-invites')
.container
- - if @dossiers.present?
- %table.table.dossiers-table.hoverable
- %thead
- %tr
- %th.number-col Nº dossier
- %th Démarche
- - if @dossiers.count > 1
- %th Demandeur
- %th.status-col Statut
- %th.updated-at-col Mis à jour
- %th.sr-only Actions
- %tbody
- - @dossiers.each do |dossier|
- %tr{ data: { 'dossier-id': dossier.id } }
- %td.number-col
- = link_to(url_for_dossier(dossier), class: 'cell-link', tabindex: -1) do
- %span.icon.folder
- = dossier.id
- %td
- = link_to(url_for_dossier(dossier), class: 'cell-link') do
- = procedure_libelle(dossier.procedure)
- - if @dossiers.count > 1
- %td.cell-link
- = demandeur_dossier(dossier)
- %td.status-col
- = status_badge(dossier.state)
- %td.updated-at-col.cell-link
- = try_format_date(dossier.updated_at)
- %td.action-col
- = render partial: 'dossier_actions', locals: { dossier: dossier }
- = paginate(@dossiers)
+ - if @statut == "mes-dossiers"
+ = render partial: "dossiers_list", locals: { dossiers: @user_dossiers }
- - if current_user.feedbacks.empty? || current_user.feedbacks.last.created_at < 1.month.ago
- #user-satisfaction
- %h3 Que pensez-vous de la facilité d'utilisation de ce service ?
- .icons
- = link_to feedback_path(rating: Feedback.ratings.fetch(:unhappy)), data: { remote: true, method: :post } do
- %span.icon.frown
- = link_to feedback_path(rating: Feedback.ratings.fetch(:neutral)), data: { remote: true, method: :post } do
- %span.icon.meh
- = link_to feedback_path(rating: Feedback.ratings.fetch(:happy)), data: { remote: true, method: :post } do
- %span.icon.smile
+ - if @statut == "dossiers-invites"
+ = render partial: "dossiers_list", locals: { dossiers: @dossiers_invites }
- - else
- .blank-tab
- %h2.empty-text Aucun dossier.
- %p.empty-text-details
- Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche.
- %br
- Celui ci doit ressembler à #{APPLICATION_BASE_URL}/commencer/xxx.
+ - if @statut == "dossiers-supprimes"
+ = render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers_supprimes }
diff --git a/config/env.example.optional b/config/env.example.optional
index 7587634fb..7707b8501 100644
--- a/config/env.example.optional
+++ b/config/env.example.optional
@@ -24,6 +24,12 @@ APPLICATION_BASE_URL="https://www.demarches-simplifiees.fr"
# Personnalisation d'instance - URL pour la création de compte administrateur sur l'instance
# DEMANDE_INSCRIPTION_ADMIN_PAGE_URL=""
+# Personnalisation d'instance - URL du site web de documentation
+# DOC_URL="https://doc.demarches-simplifiees.fr"
+
+# Personnalisation d'instance - URL du site web FAQ
+# FAQ_URL="https://faq.demarches-simplifiees.fr"
+
# Personnalisation d'instance - Page externe "Disponibilité" (status page)
# STATUS_PAGE_URL=""
diff --git a/config/initializers/urls.rb b/config/initializers/urls.rb
index 82ff12802..84053627e 100644
--- a/config/initializers/urls.rb
+++ b/config/initializers/urls.rb
@@ -16,7 +16,7 @@ FOG_BASE_URL = "https://static.demarches-simplifiees.fr"
WEBINAIRE_URL = "https://app.livestorm.co/demarches-simplifiees"
CALENDLY_URL = "https://calendly.com/demarches-simplifiees/accompagnement-administrateur-demarches-simplifiees-fr"
-DOC_URL = "https://doc.demarches-simplifiees.fr"
+DOC_URL = ENV.fetch("DOC_URL", "https://doc.demarches-simplifiees.fr")
DOC_NOUVEAUTES_URL = [DOC_URL, "nouveautes"].join("/")
ADMINISTRATEUR_TUTORIAL_URL = [DOC_URL, "tutoriels", "tutoriel-administrateur"].join("/")
INSTRUCTEUR_TUTORIAL_URL = [DOC_URL, "tutoriels", "tutoriel-accompagnateur"].join("/")
@@ -29,7 +29,7 @@ WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/")
ARCHIVAGE_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "archivage-longue-duree-des-demarches"].join("/")
DOC_INTEGRATION_MONAVIS_URL = [DOC_URL, "tutoriels", "integration-du-bouton-mon-avis"].join("/")
-FAQ_URL = "https://faq.demarches-simplifiees.fr"
+FAQ_URL = ENV.fetch("FAQ_URL", "https://faq.demarches-simplifiees.fr")
FAQ_ADMIN_URL = [FAQ_URL, "collection", "1-administrateur-creation-dun-formulaire"].join("/")
FAQ_AUTOSAVE_URL = [FAQ_URL, "article", "77-enregistrer-mon-formulaire-pour-le-reprendre-plus-tard?preview=5ec28ca1042863474d1aee00"].join("/")
COMMENT_TROUVER_MA_DEMARCHE_URL = [FAQ_URL, "article", "59-comment-trouver-ma-demarche"].join("/")
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 2b2a198f2..b3de0f42d 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -60,11 +60,15 @@ fr:
activerecord:
attributes:
+ default_attributes: &default_attributes
+ password: 'Le mot de passe'
user:
siret: 'Numéro SIRET'
- password: 'Le mot de passe'
+ << : *default_attributes
instructeur:
- password: 'Le mot de passe'
+ << : *default_attributes
+ super_admin:
+ << : *default_attributes
errors:
messages:
not_a_phone: 'Numéro de téléphone invalide'
@@ -80,7 +84,7 @@ fr:
email:
invalid: invalide
taken: déjà utilisé
- password:
+ password: &password
too_short: 'est trop court'
not_strong: 'n’est pas assez complexe'
password_confirmation:
@@ -96,6 +100,10 @@ fr:
taken: déjà utilisé
password:
too_short: 'est trop court'
+ super_admin:
+ attributes:
+ password:
+ << : *password
procedure:
attributes:
path:
@@ -161,6 +169,18 @@ fr:
zero: archivé
one: archivé
other: archivés
+ mes_dossiers:
+ zero: mon dossier
+ one: mon dossier
+ other: mes dossiers
+ dossiers_invites:
+ zero: dossier invité
+ one: dossier invité
+ other: dossiers invités
+ dossiers_supprimes:
+ zero: dossier supprimé
+ one: dossier supprimé
+ other: dossiers supprimés
dossier_trouve:
zero: 0 dossier trouvé
one: 1 dossier trouvé
diff --git a/config/locales/models/deleted_dossier/fr.yml b/config/locales/models/deleted_dossier/fr.yml
index b4232fba2..87dd5a670 100644
--- a/config/locales/models/deleted_dossier/fr.yml
+++ b/config/locales/models/deleted_dossier/fr.yml
@@ -3,9 +3,10 @@ fr:
attributes:
deleted_dossier:
reason:
- user_request: Demande d’usager
+ user_request: Demande de l’usager
manager_request: Demande d’administration
user_removed: Suppression d’un compte usager
procedure_removed: Suppression d’une démarche
expired: Expiration
unknown: Inconnue
+ instructeur_request: Suppression par l’instructeur
diff --git a/config/locales/views/dossier_mailer/notify_deletion_to_user/fr.yml b/config/locales/views/dossier_mailer/notify_deletion_to_user/fr.yml
index 5d8caecf7..ea104b332 100644
--- a/config/locales/views/dossier_mailer/notify_deletion_to_user/fr.yml
+++ b/config/locales/views/dossier_mailer/notify_deletion_to_user/fr.yml
@@ -3,3 +3,9 @@ fr:
notify_deletion_to_user:
subject: Votre dossier nº %{dossier_id} a bien été supprimé
body: Votre dossier n° %{dossier_id} (%{procedure}) a bien été supprimé. Une trace de ce traitement sera conservée pour l’administration.
+ notify_instructeur_deletion_to_user:
+ subject: Votre dossier sur la démarche « %{libelle_demarche} » est supprimé
+ body_html: |
+ Afin de limiter la conservation de vos données personnelles, votre dossier n° %{dossier_id} concernant la démarche « %{libelle_demarche} » est supprimé.
+ Cette suppression ne modifie pas le statut final (accepté, refusé ou sans suite) de votre dossier.
+ Une trace de ce dossier est visible dans votre interface : %{deleted_dossiers_link}.
diff --git a/config/locales/views/support/index.en.yml b/config/locales/views/support/index.en.yml
index f804c581c..405ea15ce 100644
--- a/config/locales/views/support/index.en.yml
+++ b/config/locales/views/support/index.en.yml
@@ -38,9 +38,9 @@ en:
contact_team: Contact our team
pro_phone_number: Professional phone number (direct line)
pro_mail: Professional email address
- admin demande rdv: I request an appointment for an online presentation of demarches-simplifiees.fr
- admin question: I have a question about demarches-simplifiees.fr
- admin soucis: I am facing a technical issue on demarches-simplifiees.fr
+ admin demande rdv: I request an appointment for an online presentation of %{app_name}
+ admin question: I have a question about %{app_name}
+ admin soucis: I am facing a technical issue on %{app_name}
admin suggestion produit: I have a suggestion for an evolution
admin demande compte: I want to open an admin account with an Orange, Wanadoo, etc. email
admin autre: Other topic
diff --git a/config/locales/views/support/index.fr.yml b/config/locales/views/support/index.fr.yml
index 0043277ca..e1dfd67ff 100644
--- a/config/locales/views/support/index.fr.yml
+++ b/config/locales/views/support/index.fr.yml
@@ -38,9 +38,9 @@ fr:
contact_team: Contactez notre équipe
pro_phone_number: Numéro de téléphone professionnel (ligne directe)
pro_mail: Adresse e-mail professionnelle
- admin demande rdv: Demande de RDV pour une présentation à distance de demarches-simplifiees.fr
- admin question: J’ai une question sur demarches-simplifiees.fr
- admin soucis: J’ai un problème technique avec demarches-simplifiees.fr
+ admin demande rdv: Demande de RDV pour une présentation à distance de %{app_name}
+ admin question: J’ai une question sur %{app_name}
+ admin soucis: J’ai un problème technique avec %{app_name}
admin suggestion produit: J’ai une proposition d’évolution
admin demande compte: Je souhaite ouvrir un compte administrateur avec un email Orange, Wanadoo, etc.
admin autre: Autre sujet
diff --git a/config/routes.rb b/config/routes.rb
index bd385df4f..cfa066dc7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -84,6 +84,10 @@ Rails.application.routes.draw do
passwords: 'super_admins/passwords'
}
+ devise_scope :super_admin do
+ get '/super_admins/password/test_strength' => 'super_admins/passwords#test_strength'
+ end
+
get 'super_admins/edit_otp', to: 'super_admins#edit_otp', as: 'edit_super_admin_otp'
put 'super_admins/enable_otp', to: 'super_admins#enable_otp', as: 'enable_super_admin_otp'
@@ -342,6 +346,7 @@ Rails.application.routes.draw do
patch 'unfollow'
patch 'archive'
patch 'unarchive'
+ patch 'supprimer-dossier' => 'dossiers#delete_dossier'
patch 'annotations' => 'dossiers#update_annotations'
post 'commentaire' => 'dossiers#create_commentaire'
post 'passer-en-instruction' => 'dossiers#passer_en_instruction'
diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb
index 379bb84d9..f95471b26 100644
--- a/spec/controllers/instructeurs/dossiers_controller_spec.rb
+++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb
@@ -306,7 +306,7 @@ describe Instructeurs::DossiersController, type: :controller do
context 'when the dossier has an attestation' do
before do
attestation = Attestation.new
- allow(attestation).to receive(:pdf).and_return(double(read: 'pdf', size: 2.megabytes))
+ allow(attestation).to receive(:pdf).and_return(double(read: 'pdf', size: 2.megabytes, attached?: false))
allow(attestation).to receive(:pdf_url).and_return('http://some_document_url')
allow_any_instance_of(Dossier).to receive(:build_attestation).and_return(attestation)
@@ -715,4 +715,69 @@ describe Instructeurs::DossiersController, type: :controller do
end
end
end
+
+ describe "#delete_dossier" do
+ subject do
+ patch :delete_dossier, params: {
+ procedure_id: procedure.id,
+ dossier_id: dossier.id
+ }
+ end
+
+ before do
+ dossier.passer_en_instruction(instructeur)
+ end
+
+ context 'just before delete the dossier, the operation must be equal to 2' do
+ before do
+ dossier.accepter!(instructeur, 'le dossier est correct')
+ end
+
+ it 'has 2 operations logs before deletion' do
+ expect(DossierOperationLog.where(dossier_id: dossier.id).count).to eq(2)
+ end
+ end
+
+ context 'when the instructeur want to delete a dossier with a decision' do
+ before do
+ dossier.accepter!(instructeur, "le dossier est correct")
+ allow(DossierMailer).to receive(:notify_instructeur_deletion_to_user).and_return(double(deliver_later: nil))
+ subject
+ end
+
+ it 'deletes previous logs and add a suppression log' do
+ expect(DossierOperationLog.where(dossier_id: dossier.id).count).to eq(3)
+ expect(DossierOperationLog.where(dossier_id: dossier.id).last.operation).to eq('supprimer')
+ end
+
+ it 'send an email to the user' do
+ expect(DossierMailer).to have_received(:notify_instructeur_deletion_to_user).with(DeletedDossier.where(dossier_id: dossier.id).first, dossier.user.email)
+ end
+
+ it 'add a record into deleted_dossiers table' do
+ expect(DeletedDossier.where(dossier_id: dossier.id).count).to eq(1)
+ expect(DeletedDossier.where(dossier_id: dossier.id).first.revision_id).to eq(dossier.revision_id)
+ expect(DeletedDossier.where(dossier_id: dossier.id).first.user_id).to eq(dossier.user_id)
+ expect(DeletedDossier.where(dossier_id: dossier.id).first.groupe_instructeur_id).to eq(dossier.groupe_instructeur_id)
+ end
+
+ it 'discard the dossier' do
+ expect(dossier.reload.hidden_at).not_to eq(nil)
+ end
+ end
+
+ context 'when the instructeur want to delete a dossier without a decision' do
+ before do
+ subject
+ end
+
+ it 'does not delete the dossier' do
+ expect { dossier.reload }.not_to raise_error ActiveRecord::RecordNotFound
+ end
+
+ it 'does not add a record into deleted_dossiers table' do
+ expect(DeletedDossier.where(dossier_id: dossier.id).count).to eq(0)
+ end
+ end
+ end
end
diff --git a/spec/controllers/super_admins/passwords_controller_spec.rb b/spec/controllers/super_admins/passwords_controller_spec.rb
new file mode 100644
index 000000000..d2e7c2b08
--- /dev/null
+++ b/spec/controllers/super_admins/passwords_controller_spec.rb
@@ -0,0 +1,12 @@
+describe SuperAdmins::PasswordsController, type: :controller do
+ describe '#test_strength' do
+ it 'calculate score' do
+ password = "bonjour"
+ @request.env["devise.mapping"] = Devise.mappings[:super_admin]
+
+ get 'test_strength', xhr: true, params: { super_admin: { password: password } }
+
+ expect(assigns(:score)).to be_present
+ end
+ end
+end
diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb
index 052b9c96e..b7208be2d 100644
--- a/spec/controllers/users/dossiers_controller_spec.rb
+++ b/spec/controllers/users/dossiers_controller_spec.rb
@@ -743,16 +743,15 @@ describe Users::DossiersController, type: :controller do
context 'when the user does not have any dossiers' do
before { get(:index) }
- it { expect(assigns(:current_tab)).to eq('mes-dossiers') }
+ it { expect(assigns(:statut)).to eq('mes-dossiers') }
end
context 'when the user only have its own dossiers' do
let!(:own_dossier) { create(:dossier, user: user) }
before { get(:index) }
-
- it { expect(assigns(:current_tab)).to eq('mes-dossiers') }
- it { expect(assigns(:dossiers)).to match([own_dossier]) }
+ it { expect(assigns(:statut)).to eq('mes-dossiers') }
+ it { expect(assigns(:user_dossiers)).to match([own_dossier]) }
end
context 'when the user only have some dossiers invites' do
@@ -760,30 +759,30 @@ describe Users::DossiersController, type: :controller do
before { get(:index) }
- it { expect(assigns(:current_tab)).to eq('dossiers-invites') }
- it { expect(assigns(:dossiers)).to match([invite.dossier]) }
+ it { expect(assigns(:statut)).to eq('dossiers-invites') }
+ it { expect(assigns(:dossiers_invites)).to match([invite.dossier]) }
end
context 'when the user has both' do
let!(:own_dossier) { create(:dossier, user: user) }
let!(:invite) { create(:invite, dossier: create(:dossier), user: user) }
- context 'and there is no current_tab param' do
+ context 'and there is no statut param' do
before { get(:index) }
- it { expect(assigns(:current_tab)).to eq('mes-dossiers') }
+ it { expect(assigns(:statut)).to eq('mes-dossiers') }
end
context 'and there is "dossiers-invites" param' do
- before { get(:index, params: { current_tab: 'dossiers-invites' }) }
+ before { get(:index, params: { statut: 'dossiers-invites' }) }
- it { expect(assigns(:current_tab)).to eq('dossiers-invites') }
+ it { expect(assigns(:statut)).to eq('dossiers-invites') }
end
context 'and there is "mes-dossiers" param' do
- before { get(:index, params: { current_tab: 'mes-dossiers' }) }
+ before { get(:index, params: { statut: 'mes-dossiers' }) }
- it { expect(assigns(:current_tab)).to eq('mes-dossiers') }
+ it { expect(assigns(:statut)).to eq('mes-dossiers') }
end
end
diff --git a/spec/factories/dossier.rb b/spec/factories/dossier.rb
index cb10c08b7..10c590a14 100644
--- a/spec/factories/dossier.rb
+++ b/spec/factories/dossier.rb
@@ -111,6 +111,9 @@ FactoryBot.define do
end
end
+ trait :brouillon do
+ end
+
trait :en_construction do
after(:create) do |dossier, _evaluator|
dossier.state = Dossier.states.fetch(:en_construction)
diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb
index 3e05e2b55..f74e2da10 100644
--- a/spec/factories/procedure.rb
+++ b/spec/factories/procedure.rb
@@ -170,6 +170,12 @@ FactoryBot.define do
end
end
+ trait :with_titre_identite do
+ after(:build) do |procedure, _evaluator|
+ build(:type_de_champ_titre_identite, procedure: procedure)
+ end
+ end
+
trait :with_repetition do
after(:build) do |procedure, _evaluator|
build(:type_de_champ_repetition, :with_types_de_champ, procedure: procedure)
diff --git a/spec/factories/super_admin.rb b/spec/factories/super_admin.rb
index 0a4f5472e..65c0d265b 100644
--- a/spec/factories/super_admin.rb
+++ b/spec/factories/super_admin.rb
@@ -2,7 +2,7 @@ FactoryBot.define do
sequence(:super_admin_email) { |n| "plop#{n}@plop.com" }
factory :super_admin do
email { generate(:super_admin_email) }
- password { 'my-s3cure-p4ssword' }
+ password { '{My-$3cure-p4ssWord}' }
otp_required_for_login { true }
end
end
diff --git a/spec/features/instructeurs/instruction_spec.rb b/spec/features/instructeurs/instruction_spec.rb
index 22e083ad4..573a562b8 100644
--- a/spec/features/instructeurs/instruction_spec.rb
+++ b/spec/features/instructeurs/instruction_spec.rb
@@ -146,6 +146,7 @@ feature 'Instructing a dossier:' do
let(:commentaire) { create(:commentaire, instructeur: instructeur, dossier: dossier) }
before do
+ dossier.passer_en_instruction!(instructeur)
champ.piece_justificative_file.attach(io: File.open(path), filename: "piece_justificative_0.pdf", content_type: "application/pdf")
log_in(instructeur.email, password)
@@ -163,9 +164,10 @@ feature 'Instructing a dossier:' do
files = ZipTricks::FileReader.read_zip_structure(io: File.open(DownloadHelpers.download))
expect(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip"
- expect(files.size).to be 1
+ expect(files.size).to be 2
expect(files[0].filename.include?('piece_justificative_0')).to be_truthy
expect(files[0].uncompressed_size).to be File.size(path)
+ expect(files[1].filename.include?('horodatage/operation')).to be_truthy
end
scenario 'A instructeur can download an archive containing several identical attachments' do
@@ -176,12 +178,13 @@ feature 'Instructing a dossier:' do
files = ZipTricks::FileReader.read_zip_structure(io: File.open(DownloadHelpers.download))
expect(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip"
- expect(files.size).to be 2
+ expect(files.size).to be 3
expect(files[0].filename.include?('piece_justificative_0')).to be_truthy
expect(files[1].filename.include?('piece_justificative_0')).to be_truthy
expect(files[0].filename).not_to eq files[1].filename
expect(files[0].uncompressed_size).to be File.size(path)
expect(files[1].uncompressed_size).to be File.size(path)
+ expect(files[2].filename.include?('horodatage/operation')).to be_truthy
end
after { DownloadHelpers.clear_downloads }
diff --git a/spec/jobs/cron/discarded_dossiers_deletion_job_spec.rb b/spec/jobs/cron/discarded_dossiers_deletion_job_spec.rb
new file mode 100644
index 000000000..61078f168
--- /dev/null
+++ b/spec/jobs/cron/discarded_dossiers_deletion_job_spec.rb
@@ -0,0 +1,68 @@
+RSpec.describe Cron::DiscardedDossiersDeletionJob, type: :job do
+ describe '#perform' do
+ let(:instructeur) { create(:instructeur) }
+ let(:dossier) { create(:dossier, state, hidden_at: hidden_at) }
+
+ before do
+ # hack to add passer_en_instruction and supprimer to dossier.dossier_operation_logs
+ dossier.send(:log_dossier_operation, instructeur, :passer_en_instruction, dossier)
+ dossier.send(:log_dossier_operation, instructeur, :supprimer, dossier)
+
+ Cron::DiscardedDossiersDeletionJob.perform_now
+ end
+
+ def operations_left
+ DossierOperationLog.where(dossier_id: dossier.id).pluck(:operation)
+ end
+
+ RSpec.shared_examples "does not delete" do
+ it 'does not delete it' do
+ expect { dossier.reload }.not_to raise_error
+ end
+
+ it 'does not delete its operations logs' do
+ expect(operations_left).to match_array(["passer_en_instruction", "supprimer"])
+ end
+ end
+
+ RSpec.shared_examples "does delete" do
+ it 'does delete it' do
+ expect { dossier.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'deletes its operations logs except supprimer' do
+ expect(operations_left).to eq(["supprimer"])
+ end
+ end
+
+ [:brouillon, :en_construction, :en_instruction, :accepte, :refuse, :sans_suite].each do |state|
+ context "with a dossier #{state}" do
+ let(:state) { state }
+
+ context 'not hidden' do
+ let(:hidden_at) { nil }
+
+ include_examples "does not delete"
+ end
+
+ context 'hidden not so long ago' do
+ let(:hidden_at) { 1.week.ago + 1.hour }
+
+ include_examples "does not delete"
+ end
+ end
+ end
+
+ [:en_construction, :accepte, :refuse, :sans_suite].each do |state|
+ context "with a dossier #{state}" do
+ let(:state) { state }
+
+ context 'hidden long ago' do
+ let(:hidden_at) { 1.week.ago - 1.hour }
+
+ include_examples "does delete"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/mailers/previews/dossier_mailer_preview.rb b/spec/mailers/previews/dossier_mailer_preview.rb
index d55c5b4e2..85a7d6252 100644
--- a/spec/mailers/previews/dossier_mailer_preview.rb
+++ b/spec/mailers/previews/dossier_mailer_preview.rb
@@ -49,6 +49,10 @@ class DossierMailerPreview < ActionMailer::Preview
DossierMailer.notify_deletion_to_user(deleted_dossier, usager_email)
end
+ def notify_instructeur_deletion_to_user
+ DossierMailer.notify_instructeur_deletion_to_user(deleted_dossier, usager_email)
+ end
+
def notify_deletion_to_administration
DossierMailer.notify_deletion_to_administration(deleted_dossier, administration_email)
end
diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb
index 0e27d568a..43d171d5e 100644
--- a/spec/models/dossier_spec.rb
+++ b/spec/models/dossier_spec.rb
@@ -1268,8 +1268,8 @@ describe Dossier do
end
end
- it { expect(Dossier.discarded_brouillon_expired.count).to eq(2) }
- it { expect(Dossier.discarded_en_construction_expired.count).to eq(2) }
+ it { expect(Dossier.discarded_brouillon_expired.count).to eq(3) }
+ it { expect(Dossier.discarded_en_construction_expired.count).to eq(3) }
end
describe "discarded procedure dossier should be able to access it's procedure" do
diff --git a/spec/models/super_admin_spec.rb b/spec/models/super_admin_spec.rb
index 606b6078a..19cf1f8d3 100644
--- a/spec/models/super_admin_spec.rb
+++ b/spec/models/super_admin_spec.rb
@@ -61,4 +61,43 @@ describe SuperAdmin, type: :model do
expect { subject }.to change { super_admin.reload.otp_secret }.to(nil)
end
end
+
+ describe '#password_complexity' do
+ # This password list is sorted by password complexity, according to zxcvbn (used for complexity evaluation)
+ # 0 - too guessable: risky password. (guesses < 10^3)
+ # 1 - very guessable: protection from throttled online attacks. (guesses < 10^6)
+ # 2 - somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)
+ # 3 - safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)
+ # 4 - very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)
+ passwords = ['pass', '12pass23', 'démarches ', 'démarches-simple', '{My-$3cure-p4ssWord}']
+ min_complexity = PASSWORD_COMPLEXITY_FOR_ADMIN
+
+ let(:email) { 'mail@beta.gouv.fr' }
+ let(:super_admin) { build(:super_admin, email: email, password: password) }
+
+ subject do
+ super_admin.save
+ super_admin.errors.full_messages
+ end
+
+ context 'when password is too short' do
+ let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
+
+ it { expect(subject).to eq(["Le mot de passe est trop court"]) }
+ end
+
+ context 'when password is too simple' do
+ passwords[0..(min_complexity - 1)].each do |password|
+ let(:password) { password }
+
+ it { expect(subject).to eq(["Le mot de passe n’est pas assez complexe"]) }
+ end
+ end
+
+ context 'when password is acceptable' do
+ let(:password) { passwords[min_complexity] }
+
+ it { expect(subject).to eq([]) }
+ end
+ end
end
diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb
new file mode 100644
index 000000000..d0ad55499
--- /dev/null
+++ b/spec/services/pieces_justificatives_service_spec.rb
@@ -0,0 +1,22 @@
+describe PiecesJustificativesService do
+ describe '.liste_pieces_justificatives' do
+ let(:procedure) { create(:procedure, :with_titre_identite) }
+ let(:dossier) { create(:dossier, procedure: procedure) }
+ let(:champ_identite) { dossier.champs.find { |c| c.type == 'Champs::TitreIdentiteChamp' } }
+
+ before do
+ champ_identite
+ .piece_justificative_file
+ .attach(io: StringIO.new("toto"), filename: "toto.png", content_type: "image/png")
+ end
+
+ subject { PiecesJustificativesService.liste_pieces_justificatives(dossier) }
+
+ # titre identite is too sensitive
+ # to be exported
+ it 'ensures no titre identite is given' do
+ expect(champ_identite.piece_justificative_file).to be_attached
+ expect(subject).to eq([])
+ end
+ end
+end
diff --git a/spec/views/instructeur/dossiers/_state_button.html.haml_spec.rb b/spec/views/instructeur/dossiers/_state_button.html.haml_spec.rb
index e320d2464..d44baf449 100644
--- a/spec/views/instructeur/dossiers/_state_button.html.haml_spec.rb
+++ b/spec/views/instructeur/dossiers/_state_button.html.haml_spec.rb
@@ -70,7 +70,7 @@ describe 'instructeurs/dossiers/state_button.html.haml', type: :view do
it 'renders a dropdown' do
expect(rendered).to have_dropdown_title(dossier_display_state(dossier))
- expect(rendered).to have_dropdown_items(count: 1)
+ expect(rendered).to have_dropdown_items(count: 2)
expect(rendered).to have_dropdown_item('Repasser en instruction', href: repasser_en_instruction_instructeur_dossier_path(dossier.procedure, dossier))
end
diff --git a/spec/views/users/dossiers/index.html.haml_spec.rb b/spec/views/users/dossiers/index.html.haml_spec.rb
index ff91ccce5..e3d2514e5 100644
--- a/spec/views/users/dossiers/index.html.haml_spec.rb
+++ b/spec/views/users/dossiers/index.html.haml_spec.rb
@@ -4,15 +4,15 @@ describe 'users/dossiers/index.html.haml', type: :view do
let(:dossier_en_construction) { create(:dossier, state: Dossier.states.fetch(:en_construction), user: user) }
let(:user_dossiers) { [dossier_brouillon, dossier_en_construction] }
let(:dossiers_invites) { [] }
- let(:current_tab) { 'mes-dossiers' }
+ let(:statut) { 'mes-dossiers' }
before do
allow(view).to receive(:new_demarche_url).and_return('#')
allow(controller).to receive(:current_user) { user }
assign(:user_dossiers, Kaminari.paginate_array(user_dossiers).page(1))
assign(:dossiers_invites, Kaminari.paginate_array(dossiers_invites).page(1))
- assign(:dossiers, Kaminari.paginate_array(user_dossiers).page(1))
- assign(:current_tab, current_tab)
+ assign(:dossiers_supprimes, Kaminari.paginate_array(user_dossiers).page(1))
+ assign(:statut, statut)
render
end
@@ -48,11 +48,11 @@ describe 'users/dossiers/index.html.haml', type: :view do
let(:dossiers_invites) { [] }
it 'affiche un titre adapté' do
- expect(rendered).to have_selector('h1', text: 'Mes dossiers')
+ expect(rendered).to have_selector('h1', text: 'Dossiers')
end
- it 'n’affiche pas la barre d’onglets' do
- expect(rendered).not_to have_selector('ul.tabs')
+ it 'n’affiche la barre d’onglets' do
+ expect(rendered).to have_selector('ul.tabs')
end
end
@@ -65,7 +65,7 @@ describe 'users/dossiers/index.html.haml', type: :view do
it 'affiche la barre d’onglets' do
expect(rendered).to have_selector('ul.tabs')
- expect(rendered).to have_selector('ul.tabs li', count: 2)
+ expect(rendered).to have_selector('ul.tabs li', count: 3)
expect(rendered).to have_selector('ul.tabs li.active', count: 1)
end
end