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