Merge pull request #6833 from betagouv/main

2022-01-25-01
This commit is contained in:
Kara Diaby 2022-01-25 14:23:49 +01:00 committed by GitHub
commit 4778eaaa9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 433 additions and 427 deletions

View file

@ -54,6 +54,10 @@
font-size: 14px;
}
.text-lg {
font-size: 18px;
}
.mt-1 {
margin-top: $default-spacer;
}

View file

@ -3,12 +3,17 @@ module Administrateurs
before_action :retrieve_procedure
def edit
@attestation_template = @procedure.attestation_template || AttestationTemplate.new(procedure: @procedure)
@attestation_template = build_attestation
end
def update
attestation_template = @procedure.attestation_template
attestation_template = @procedure.draft_attestation_template.revise!
if attestation_template.update(activated_attestation_params)
AttestationTemplate
.where(id: @procedure.revisions.pluck(:attestation_template_id).compact)
.update_all(activated: attestation_template.activated?)
flash.notice = "L'attestation a bien été modifiée"
else
flash.alert = attestation_template.errors.full_messages.join('<br>')
@ -18,7 +23,7 @@ module Administrateurs
end
def create
attestation_template = AttestationTemplate.new(activated_attestation_params.merge(procedure_id: @procedure.id))
attestation_template = build_attestation(activated_attestation_params)
if attestation_template.save
flash.notice = "L'attestation a bien été sauvegardée"
@ -30,14 +35,19 @@ module Administrateurs
end
def preview
attestation = @procedure.attestation_template || AttestationTemplate.new
@attestation = attestation.render_attributes_for({})
@attestation = build_attestation.render_attributes_for({})
render 'administrateurs/attestation_templates/show', formats: [:pdf]
end
private
def build_attestation(attributes = {})
attestation_template = @procedure.draft_attestation_template || @procedure.draft_revision.build_attestation_template
attestation_template.attributes = attributes
attestation_template
end
def activated_attestation_params
# cache result to avoid multiple uninterlaced computations
if @activated_attestation_params.nil?

View file

@ -208,34 +208,33 @@ module Administrateurs
end
def import
if !CSV_ACCEPTED_CONTENT_TYPES.include?(group_csv_file.content_type) && !CSV_ACCEPTED_CONTENT_TYPES.include?(marcel_content_type)
flash[:alert] = "Importation impossible : veuillez importer un fichier CSV"
redirect_to admin_procedure_groupe_instructeurs_path(procedure)
if procedure.publiee?
if !CSV_ACCEPTED_CONTENT_TYPES.include?(group_csv_file.content_type) && !CSV_ACCEPTED_CONTENT_TYPES.include?(marcel_content_type)
flash[:alert] = "Importation impossible : veuillez importer un fichier CSV"
elsif group_csv_file.size > CSV_MAX_SIZE
flash[:alert] = "Importation impossible : le poids du fichier est supérieur à #{number_to_human_size(CSV_MAX_SIZE)}"
redirect_to admin_procedure_groupe_instructeurs_path(procedure)
elsif group_csv_file.size > CSV_MAX_SIZE
flash[:alert] = "Importation impossible : le poids du fichier est supérieur à #{number_to_human_size(CSV_MAX_SIZE)}"
else
file = group_csv_file.read
base_encoding = CharlockHolmes::EncodingDetector.detect(file)
groupes_emails = ACSV::CSV.new_for_ruby3(file.encode("UTF-8", base_encoding[:encoding], invalid: :replace, replace: ""), headers: true, header_converters: :downcase)
.map { |r| r.to_h.slice('groupe', 'email') }
groupes_emails_has_keys = groupes_emails.first.has_key?("groupe") && groupes_emails.first.has_key?("email")
if groupes_emails_has_keys.blank?
flash[:alert] = "Importation impossible, veuillez importer un csv #{view_context.link_to('suivant ce modèle', "/csv/#{I18n.locale}/import-groupe-test.csv")}"
else
add_instructeurs_and_get_errors = InstructeursImportService.import(procedure, groupes_emails)
file = group_csv_file.read
base_encoding = CharlockHolmes::EncodingDetector.detect(file)
groupes_emails = ACSV::CSV.new_for_ruby3(file.encode("UTF-8", base_encoding[:encoding], invalid: :replace, replace: ""), headers: true, header_converters: :downcase)
.map { |r| r.to_h.slice('groupe', 'email') }
if add_instructeurs_and_get_errors.empty?
flash[:notice] = "La liste des instructeurs a été importée avec succès"
groupes_emails_has_keys = groupes_emails.first.has_key?("groupe") && groupes_emails.first.has_key?("email")
if groupes_emails_has_keys.blank?
flash[:alert] = "Importation impossible, veuillez importer un csv #{view_context.link_to('suivant ce modèle', "/csv/#{I18n.locale}/import-groupe-test.csv")}"
else
flash[:alert] = "Import terminé. Cependant les emails suivants ne sont pas pris en compte: #{add_instructeurs_and_get_errors.join(', ')}"
add_instructeurs_and_get_errors = InstructeursImportService.import(procedure, groupes_emails)
if add_instructeurs_and_get_errors.empty?
flash[:notice] = "La liste des instructeurs a été importée avec succès"
else
flash[:alert] = "Import terminé. Cependant les emails suivants ne sont pas pris en compte: #{add_instructeurs_and_get_errors.join(', ')}"
end
end
end
redirect_to admin_procedure_groupe_instructeurs_path(procedure)
end
end

View file

@ -276,8 +276,9 @@ class ApplicationController < ActionController::Base
matomo = Rails.application.secrets.matomo
{
key: matomo[:client_key],
enabled: matomo[:enabled]
enabled: matomo[:enabled],
host: matomo[:host],
key: matomo[:client_key]
}
end

View file

@ -13,7 +13,7 @@ module Instructeurs
.order(closed_at: :desc, unpublished_at: :desc, published_at: :desc, created_at: :desc)
dossiers = current_instructeur.dossiers.joins(:groupe_instructeur)
@dossiers_count_per_procedure = dossiers.all_state.group('groupe_instructeurs.procedure_id').reorder(nil).count
@dossiers_count_per_procedure = dossiers.all_state.visible_by_administration.group('groupe_instructeurs.procedure_id').reorder(nil).count
@dossiers_a_suivre_count_per_procedure = dossiers.without_followers.en_cours.visible_by_administration.group('groupe_instructeurs.procedure_id').reorder(nil).count
@dossiers_archived_count_per_procedure = dossiers.archived.group('groupe_instructeurs.procedure_id').count
@dossiers_termines_count_per_procedure = dossiers.termine.visible_by_administration.group('groupe_instructeurs.procedure_id').reorder(nil).count
@ -71,7 +71,7 @@ module Instructeurs
@followed_dossiers_id = @followed_dossiers.pluck(:id)
@termines_dossiers = dossiers_visibles.termine.visible_by_administration
@all_state_dossiers = dossiers_visibles.all_state
@all_state_dossiers = dossiers_visibles.all_state.visible_by_administration
@archived_dossiers = dossiers_visibles.archived
@expirant_dossiers = dossiers_visibles.termine_or_en_construction_close_to_expiration

View file

@ -16,39 +16,6 @@ module Manager
end
end
#
# Custom actions
#
def discard
dossier = Dossier.find(params[:id])
dossier.discard_and_keep_track!(current_super_admin, :manager_request)
logger.info("Le dossier #{dossier.id} est supprimé par #{current_super_admin.email}")
flash[:notice] = "Le dossier #{dossier.id} a été supprimé."
redirect_to manager_dossier_path(dossier)
end
def restore
dossier = Dossier.with_discarded.find(params[:id])
dossier.restore(current_super_admin)
flash[:notice] = "Le dossier #{dossier.id} a été restauré."
redirect_to manager_dossier_path(dossier)
end
def repasser_en_instruction
dossier = Dossier.find(params[:id])
dossier.repasser_en_instruction(instructeur: current_super_admin)
logger.info("Le dossier #{dossier.id} est repassé en instruction par #{current_super_admin.email}")
flash[:notice] = "Le dossier #{dossier.id} est repassé en instruction."
redirect_to manager_dossier_path(dossier)
end
private
def unfiltered_list?

View file

@ -110,9 +110,17 @@ function ComboMultiple({
extraOptions[0] &&
extraOptions[0][0] == selectedValue
) {
setNewValues((newValues) => [...newValues, selectedValue]);
setNewValues((newValues) => {
const set = new Set(newValues);
set.add(selectedValue);
return [...set];
});
}
saveSelection((selections) => [...selections, selectedValue]);
saveSelection((selections) => {
const set = new Set(selections);
set.add(selectedValue);
return [...set];
});
}
setTerm('');
awaitFormSubmit.done();
@ -162,18 +170,17 @@ function ComboMultiple({
};
const onBlur = () => {
if (
const shouldSelect =
term &&
[...extraOptions, ...options].map(([label]) => label).includes(term)
) {
awaitFormSubmit(() => {
[...extraOptions, ...options].map(([label]) => label).includes(term);
awaitFormSubmit(() => {
if (shouldSelect) {
onSelect(term);
});
} else {
setTimeout(() => {
} else {
hidePopover();
}, 200);
}
}
});
};
return (
@ -232,17 +239,11 @@ function ComboMultiple({
</button>
</li>
)}
{results.map(([label, value], index) => {
{results.map(([label], index) => {
if (label.startsWith('--')) {
return <ComboboxSeparator key={index} value={label} />;
}
return (
<ComboboxOption
key={index}
value={label}
data-option-value={value}
/>
);
return <ComboboxOption key={index} value={label} />;
})}
</ComboboxList>
</ComboboxPopover>

View file

@ -122,13 +122,7 @@ function ComboSearch({
const label = getLabel(result);
const [key, value] = transformResult(result);
resultsMap.current[label] = { key, value, result };
return (
<ComboboxOption
key={`${key}-${index}`}
value={label}
data-option-value={value}
/>
);
return <ComboboxOption key={`${key}-${index}`} value={label} />;
})}
</ComboboxList>
) : (

View file

@ -1,11 +1,10 @@
const { key, enabled } = gon.matomo || {};
const { enabled, host, key } = gon.matomo || {};
if (enabled) {
window._paq = window._paq || [];
const url = '//stats.data.gouv.fr/';
const trackerUrl = `${url}piwik.php`;
const jsUrl = `${url}piwik.js`;
const jsUrl = `//${host}/piwik.js`;
const trackerUrl = `//${host}/piwik.php`;
//
// Configure Matomo analytics

View file

@ -1,10 +1,7 @@
class Cron::DiscardedProceduresDeletionJob < Cron::CronJob
self.schedule_expression = "every day at 1 am"
def perform(*args)
Procedure.discarded_expired.find_each do |procedure|
procedure.dossiers.with_discarded.destroy_all
procedure.destroy
end
def perform
Procedure.purge_discarded
end
end

View file

@ -47,7 +47,7 @@ class ActiveJob::ApplicationLogSubscriber < ::ActiveJob::LogSubscriber
end
data[:tags] = tags
data[:type] = 'tps'
data[:source] = ENV['SOURCE']
data[:source] = ENV['LOGRAGE_SOURCE']
log(data)
end

View file

@ -84,22 +84,11 @@ class DossierMailer < ApplicationMailer
mail(to: to_email, subject: @subject)
end
def notify_deletion_to_user(deleted_dossier, to_email)
I18n.with_locale(deleted_dossier.user_locale) do
@subject = default_i18n_subject(dossier_id: deleted_dossier.dossier_id)
@deleted_dossier = deleted_dossier
def notify_en_construction_deletion_to_administration(dossier, to_email)
@subject = default_i18n_subject(dossier_id: dossier.id)
@dossier = dossier
mail(to: to_email, subject: @subject)
end
end
def notify_instructeur_deletion_to_user(deleted_dossier, to_email)
I18n.with_locale(deleted_dossier.user_locale) do
@subject = default_i18n_subject(libelle_demarche: deleted_dossier.procedure.libelle)
@deleted_dossier = deleted_dossier
mail(to: to_email, subject: @subject)
end
mail(to: to_email, subject: @subject)
end
def notify_deletion_to_administration(deleted_dossier, to_email)

View file

@ -15,7 +15,8 @@ class AttestationTemplate < ApplicationRecord
include ActionView::Helpers::NumberHelper
include TagsSubstitutionConcern
belongs_to :procedure, optional: false
belongs_to :procedure, optional: true
has_many :revisions, class_name: 'ProcedureRevision', inverse_of: :attestation_template, dependent: :nullify
has_one_attached :logo
has_one_attached :signature
@ -103,6 +104,25 @@ class AttestationTemplate < ApplicationRecord
}
end
def revise!
if revisions.size > 1
attestation_template = dup
attestation_template.save!
revisions
.last
.procedure
.draft_revision
.update!(attestation_template: attestation_template)
attestation_template
else
self
end
end
def procedure
revisions.last&.procedure || super
end
private
def used_tags

View file

@ -209,7 +209,7 @@ class Dossier < ApplicationRecord
scope :hidden_by_user, -> { where.not(hidden_by_user_at: nil) }
scope :hidden_by_administration, -> { where.not(hidden_by_administration_at: nil) }
scope :visible_by_user, -> { where(hidden_by_user_at: nil) }
scope :visible_by_administration, -> { where("hidden_by_administration_at IS NULL AND NOT (hidden_by_user_at IS NOT NULL AND state = 'en_construction')") }
scope :visible_by_administration, -> { where("hidden_by_administration_at IS NULL AND NOT (hidden_by_user_at IS NOT NULL AND dossiers.state = 'en_construction')") }
scope :order_by_updated_at, -> (order = :desc) { order(updated_at: order) }
scope :order_by_created_at, -> (order = :asc) { order(depose_at: order, created_at: order, id: order) }
@ -539,10 +539,6 @@ class Dossier < ApplicationRecord
en_construction? || termine?
end
def can_be_deleted_by_manager?
kept? && can_be_deleted_by_user?
end
def messagerie_available?
!brouillon? && !user_deleted? && !archived
end
@ -704,10 +700,6 @@ class Dossier < ApplicationRecord
termine? && hidden_by_administration? && hidden_by_user?
end
def can_be_restored_by_manager?
hidden_by_administration? || discarded? && !procedure.discarded?
end
def expose_legacy_carto_api?
procedure.expose_legacy_carto_api?
end
@ -728,9 +720,11 @@ class Dossier < ApplicationRecord
{ lon: lon, lat: lat, zoom: zoom }
end
def unspecified_attestation_champs
attestation_template = procedure.attestation_template
def attestation_template
revision.attestation_template
end
def unspecified_attestation_champs
if attestation_template&.activated?
attestation_template.unspecified_champs_for_dossier(self)
else
@ -739,8 +733,8 @@ class Dossier < ApplicationRecord
end
def build_attestation
if procedure.attestation_template&.activated?
procedure.attestation_template.attestation_for(self)
if attestation_template&.activated?
attestation_template.attestation_for(self)
end
end
@ -780,7 +774,6 @@ class Dossier < ApplicationRecord
update(hidden_by_user_at: Time.zone.now, dossier_transfer_id: nil)
end
user_email = user_deleted? ? nil : user_email_for(:notification)
deleted_dossier = nil
transaction do
@ -803,14 +796,6 @@ class Dossier < ApplicationRecord
DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later
end
end
if user_email.present?
if reason == :user_request
DossierMailer.notify_deletion_to_user(deleted_dossier, user_email).deliver_later
else
DossierMailer.notify_instructeur_deletion_to_user(deleted_dossier, user_email).deliver_later
end
end
end
end

View file

@ -97,7 +97,8 @@ class Export < ApplicationRecord
end
def io(since: nil)
dossiers = Dossier.where(groupe_instructeur: groupe_instructeurs)
dossiers = Dossier.visible_by_administration
.where(groupe_instructeur: groupe_instructeurs)
if since.present?
dossiers = dossiers.where('dossiers.depose_at > ?', since)
end

View file

@ -235,7 +235,7 @@ class Instructeur < ApplicationRecord
COUNT(DISTINCT dossiers.id) FILTER (where not archived AND NOT (dossiers.hidden_by_user_at IS NOT NULL AND state = 'en_construction') AND dossiers.state in ('en_construction', 'en_instruction') AND follows.id IS NULL) AS a_suivre,
COUNT(DISTINCT dossiers.id) FILTER (where not archived AND dossiers.state in ('en_construction', 'en_instruction') AND follows.instructeur_id = :instructeur_id) AS suivis,
COUNT(DISTINCT dossiers.id) FILTER (where not archived AND dossiers.state in ('accepte', 'refuse', 'sans_suite')) AS traites,
COUNT(DISTINCT dossiers.id) FILTER (where not archived) AS tous,
COUNT(DISTINCT dossiers.id) FILTER (where not archived AND NOT (dossiers.hidden_by_user_at IS NOT NULL AND state = 'en_construction')) AS tous,
COUNT(DISTINCT dossiers.id) FILTER (where archived) AS archives,
COUNT(DISTINCT dossiers.id) FILTER (where
procedures.procedure_expires_when_termine_enabled

View file

@ -77,12 +77,15 @@ class Procedure < ApplicationRecord
has_many :published_types_de_champ_private, through: :published_revision, source: :types_de_champ_private
has_many :draft_types_de_champ, through: :draft_revision, source: :types_de_champ
has_many :draft_types_de_champ_private, through: :draft_revision, source: :types_de_champ_private
has_one :draft_attestation_template, through: :draft_revision, source: :attestation_template
has_one :published_attestation_template, through: :published_revision, source: :attestation_template
has_many :experts_procedures, dependent: :destroy
has_many :experts, through: :experts_procedures
has_one :module_api_carto, dependent: :destroy
has_one :attestation_template, dependent: :destroy
has_many :attestation_templates, through: :revisions, source: :attestation_template
belongs_to :parent_procedure, class_name: 'Procedure', optional: true
belongs_to :canonical_procedure, class_name: 'Procedure', optional: true
@ -373,7 +376,7 @@ class Procedure < ApplicationRecord
end
def draft_changed?
publiee? && published_revision.changed?(draft_revision) && revision_changes.present?
publiee? && published_revision.different_from?(draft_revision) && revision_changes.present?
end
def revision_changes
@ -434,7 +437,8 @@ class Procedure < ApplicationRecord
},
revision_types_de_champ_private: {
type_de_champ: :types_de_champ
}
},
attestation_template: []
}
}
include_list[:groupe_instructeurs] = :instructeurs if !is_different_admin
@ -572,13 +576,17 @@ class Procedure < ApplicationRecord
touch(:whitelisted_at)
end
def active_attestation_template
published_attestation_template || draft_attestation_template
end
def closed_mail_template_attestation_inconsistency_state
# As an optimization, dont check the predefined templates (they are presumed correct)
if closed_mail.present?
tag_present = closed_mail.body.to_s.include?("--lien attestation--")
if attestation_template&.activated? && !tag_present
if active_attestation_template&.activated? && !tag_present
:missing_tag
elsif !attestation_template&.activated? && tag_present
elsif !active_attestation_template&.activated? && tag_present
:extraneous_tag
end
end
@ -649,7 +657,7 @@ class Procedure < ApplicationRecord
end
def can_be_deleted_by_administrateur?
brouillon? || dossiers.state_instruction_commencee.empty?
brouillon? || dossiers.state_en_instruction.empty?
end
def can_be_deleted_by_manager?
@ -663,18 +671,25 @@ class Procedure < ApplicationRecord
close!
end
dossiers.each do |dossier|
dossiers.termine.visible_by_administration.each do |dossier|
dossier.discard_and_keep_track!(author, :procedure_removed)
end
discard!
end
def purge_discarded
if !dossiers.with_discarded.exists?
destroy
end
end
def self.purge_discarded
discarded_expired.find_each(&:purge_discarded)
end
def restore(author)
if discarded? && undiscard
dossiers.with_discarded.discarded.find_each do |dossier|
dossier.restore(author)
end
dossiers.hidden_by_administration.find_each do |dossier|
dossier.restore(author)
end

View file

@ -2,15 +2,17 @@
#
# Table name: procedure_revisions
#
# id :bigint not null, primary key
# published_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# procedure_id :bigint not null
# id :bigint not null, primary key
# published_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# attestation_template_id :bigint
# procedure_id :bigint not null
#
class ProcedureRevision < ApplicationRecord
self.implicit_order_column = :created_at
belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false
belongs_to :attestation_template, inverse_of: :revisions, optional: true, dependent: :destroy
has_many :dossiers, inverse_of: :revision, foreign_key: :revision_id
@ -105,7 +107,7 @@ class ProcedureRevision < ApplicationRecord
!draft?
end
def changed?(revision)
def different_from?(revision)
types_de_champ != revision.types_de_champ || types_de_champ_private != revision.types_de_champ_private
end
@ -125,6 +127,10 @@ class ProcedureRevision < ApplicationRecord
)
end
def attestation_template
super || procedure.attestation_template
end
private
def compare_types_de_champ(from_tdc, to_tdc)

View file

@ -117,6 +117,7 @@ class ExpiredDossiersDeletionService
def self.group_by_user_email(dossiers, notify_on_closed_procedures_to_user: false)
dossiers
.visible_by_user
.with_notifiable_procedure(notify_on_closed: notify_on_closed_procedures_to_user)
.includes(:user, :procedure)
.group_by(&:user)
@ -125,6 +126,7 @@ class ExpiredDossiersDeletionService
def self.group_by_fonctionnaire_email(dossiers)
dossiers
.visible_by_administration
.with_notifiable_procedure(notify_on_closed: true)
.includes(:followers_instructeurs, procedure: [:administrateurs])
.each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |dossier, h|

View file

@ -24,11 +24,13 @@ class ProcedureArchiveService
end
def new_collect_files_archive(archive, instructeur)
## faux, ca ne doit prendre que certains groupe instructeur ?
if archive.time_span_type == 'everything'
dossiers = @procedure.dossiers.state_termine
dossiers = Dossier.visible_by_administration
.where(groupe_instructeur: archive.groupe_instructeurs)
dossiers = if archive.time_span_type == 'everything'
dossiers.state_termine
else
dossiers = @procedure.dossiers.processed_in_month(archive.month)
dossiers.processed_in_month(archive.month)
end
attachments = create_list_of_attachments(dossiers)
@ -44,10 +46,13 @@ class ProcedureArchiveService
end
def old_collect_files_archive(archive, instructeur)
if archive.time_span_type == 'everything'
dossiers = @procedure.dossiers.state_termine
dossiers = Dossier.visible_by_administration
.where(groupe_instructeur: archive.groupe_instructeurs)
dossiers = if archive.time_span_type == 'everything'
dossiers.state_termine
else
dossiers = @procedure.dossiers.processed_in_month(archive.month)
dossiers.processed_in_month(archive.month)
end
files = create_list_of_attachments(dossiers)

View file

@ -1,7 +1,7 @@
class MonAvisEmbedValidator < ActiveModel::Validator
def validate(record)
# We need to ensure the embed code is not any random string in order to avoid injections
r = Regexp.new('<a href="https://monavis|voxusagers.numerique.gouv.fr/Demarches/\d+.*key=[[:alnum:]]+.*">\s*<img src="https://monavis|voxusagers.numerique.gouv.fr/(monavis-)?static/bouton-blanc|bleu.png|svg" alt="Je donne mon avis" title="Je donne mon avis sur cette démarche" />\s*</a>', Regexp::MULTILINE)
r = Regexp.new('<a href="https://monavis|voxusagers.numerique.gouv.fr/Demarches/\d+.*key=[[:alnum:]]+.*">\s*<img src="https://monavis|voxusagers.numerique.gouv.fr/(monavis-)?static/bouton-blanc|bleu.png|svg" alt="Je donne mon avis" (title="Je donne mon avis sur cette démarche" )?/>\s*</a>', Regexp::MULTILINE)
if record.monavis_embed.present? && !r.match?(record.monavis_embed)
record.errors[:base] << "Le code fourni ne correspond pas au format des codes MonAvis reconnus par la plateforme."
end

View file

@ -21,13 +21,17 @@
= f.submit 'Ajouter le groupe', class: "button primary send"
- csv_max_size = Administrateurs::GroupeInstructeursController::CSV_MAX_SIZE
= form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, multipart: true, class: "mt-4 form" do
= label_tag "Importer par fichier CSV"
%p.notice Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. L'import n'écrase pas les groupes et les instructeurs existants.
%p.notice Le poids du fichier doit être inférieur à #{number_to_human_size(csv_max_size)}
%p.mt-2.mb-2= link_to "Télécharger l'exemple de fichier CSV", "/csv/#{I18n.locale}/import-groupe-test.csv"
= file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1"
= submit_tag "Importer le fichier", class: 'button primary send', data: { disable_with: "Envoi..." }
- if procedure.publiee?
= form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, multipart: true, class: "mt-4 form" do
= label_tag "Importer par fichier CSV"
%p.notice Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. L'import n'écrase pas les groupes et les instructeurs existants.
%p.notice Le poids du fichier doit être inférieur à #{number_to_human_size(csv_max_size)}
%p.mt-2.mb-2= link_to "Télécharger l'exemple de fichier CSV", "/csv/#{I18n.locale}/import-groupe-test.csv"
= file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1"
= submit_tag "Importer le fichier", class: 'button primary send', data: { disable_with: "Envoi..." }
- else
%p.mt-4.form.bold.mb-2.text-lg Importer par fichier CSV
%p.notice Limport dinstructeurs par fichier CSV est disponible une fois la démarche publiée
%table.table.mt-2
%thead

View file

@ -138,7 +138,7 @@
.procedure-grid
= link_to edit_admin_procedure_attestation_template_path(@procedure), class: 'card-admin' do
- if @procedure.attestation_template.present? && @procedure.attestation_template.activated
- if @procedure.draft_attestation_template&.activated?
%div
%span.icon.accept
%p.card-admin-status-accept Activée

View file

@ -3,6 +3,6 @@
%p= t(:hello, scope: [:views, :shared, :greetings])
%p
= t('.body', dossier_id: @deleted_dossier.dossier_id, procedure: @deleted_dossier.procedure.libelle)
= t('.body', dossier_id: @dossier.id, procedure: @dossier.procedure.libelle)
= render partial: "layouts/mailers/signature"

View file

@ -1,8 +0,0 @@
- content_for(:title, "#{@subject}")
%p= t(:hello, scope: [:views, :shared, :greetings])
%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"

View file

@ -26,17 +26,6 @@ as well as a link to its edit page.
(Supprimé)
<% end %>
</h1>
<div>
<% if dossier.accepte? %>
<%= link_to 'Repasser en instruction', repasser_en_instruction_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Confirmez vous le passage en instruction du dossier ?" } %>
<% end %>
<% if dossier.can_be_deleted_by_manager? %>
<%= link_to 'Supprimer le dossier', discard_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Confirmez vous la suppression du dossier ?" } %>
<% elsif dossier.can_be_restored_by_manager? %>
<%= link_to 'Restaurer le dossier', restore_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Confirmez vous la restauration du dossier ?" } %>
<% end %>
</div>
</header>
<section class="main-content__body">

View file

@ -1,126 +1,135 @@
# Nom d'hôte de l'appli
# * Pour du dev local: localhost:3000
# * pour de la preprod: preprod.ds.organisme.fr (par exemple)
# * pour de la prod: www.demarches-simplifiees.fr
# Application host name
#
# Examples:
# * For local development: localhost:3000
# * For preproduction: staging.ds.organisme.fr
# * For production: ds.organisme.fr
APP_HOST="localhost:3000"
# Utilisé pour les logs LogRage
SOURCE="tps_local"
# Clé de chiffrement de rails, cf https://api.rubyonrails.org/classes/Rails/Application.html
# Rails key for signing sensitive data
# See https://guides.rubyonrails.org/security.html
#
# For production you MUST generate a new key, and keep it secret.
# Secrets must be long and random. Use bin/rails secret to get new unique secrets.
SECRET_KEY_BASE="05a2d479d8e412198dabd08ef0eee9d6e180f5cbb48661a35fd1cae287f0a93d40b5f1da08f06780d698bbd458a0ea97f730f83ee780de5d4e31f649a0130cf0"
SIGNING_KEY="aef3153a9829fa4ba10acb02927ac855df6b92795b1ad265d654443c4b14a017"
# Clé de chiffrement OTP, pour 2FA
# Secret key for One-Time-Password codes, used for 2-factors authentication
OTP_SECRET_KEY=""
# SAML IdP
# SAML_IDP_CERTIFICATE="billybop"
# SAML_IDP_SECRET_KEY="-----BEGIN RSA PRIVATE KEY-----\nblabla+blabla\n-----END RSA PRIVATE KEY-----\n"
# SENDINBLUE_LOGIN_URL="https://app.sendinblue.com/account/saml/login/truc"
# Database
# Database credentials
DB_DATABASE="tps_development"
DB_HOST="localhost"
DB_POOL=""
DB_USERNAME="tps_development"
DB_PASSWORD="tps_development"
# Protection simple de l'instance par mot de passe (utile pour la pre-prod)
# Protect access to the instance with a static login/password (useful for staging environments)
BASIC_AUTH_ENABLED="disabled"
BASIC_AUTH_USERNAME=""
BASIC_AUTH_PASSWORD=""
# Object Storage pour les pièces jointes
# Object Storage for attachments
FOG_ENABLED="disabled"
FOG_OPENSTACK_API_KEY=""
FOG_OPENSTACK_USERNAME=""
FOG_OPENSTACK_URL=""
FOG_OPENSTACK_REGION=""
FOG_ENABLED="" # valeur attendue: enabled
DS_PROXY_URL=""
# Service externe: authentification France Connect
# External service: authentication through France Connect
FC_PARTICULIER_ID=""
FC_PARTICULIER_SECRET=""
FC_PARTICULIER_BASE_URL=""
# Service externe: authentification Agent Connect
# External service: authentication through Agent Connect
AGENT_CONNECT_ID=""
AGENT_CONNECT_SECRET=""
AGENT_CONNECT_BASE_URL=""
AGENT_CONNECT_JWKS=""
AGENT_CONNECT_REDIRECT=""
# Service externe: Support Utilisateur HelpScout | Spécifique démarches-simplifiées.fr
# External service: integration with HelpScout (optional)
HELPSCOUT_MAILBOX_ID=""
HELPSCOUT_CLIENT_ID=""
HELPSCOUT_CLIENT_SECRET=""
HELPSCOUT_WEBHOOK_SECRET=""
# Service externe: Supervision exterieure | Spécifique démarches-simplifiées.fr
# External service: external supervision
SENTRY_ENABLED="disabled"
SENTRY_CURRENT_ENV="development"
SENTRY_DSN_RAILS=""
SENTRY_DSN_JS=""
# Statistiques web
# External service: Matomo web analytics
MATOMO_ENABLED="disabled"
MATOMO_ID="73"
# Missing MATOMO_HOST (thus hardcoded)
MATOMO_ID=""
MATOMO_HOST="matomo.organisme.fr"
# SMTP Provider: Send In Blue
SENDINBLUE_ENABLED="disabled"
SENDINBLUE_BALANCING=""
SENDINBLUE_BALANCING_VALUE=""
SENDINBLUE_ENABLED=""
SENDINBLUE_CLIENT_KEY=""
SENDINBLUE_SMTP_KEY=""
SENDINBLUE_USER_NAME=""
# Service externe: Fournisseur de tchat pour administrateur | Spécifique démarches-simplifiées.fr
CRISP_ENABLED="disabled"
CRISP_CLIENT_KEY=""
# Service externe: rattrapage de mails envoyés, utile en préprod | Spécifique démarches-simplifiées.fr
MAILTRAP_ENABLED="disabled"
MAILTRAP_USERNAME=""
MAILTRAP_PASSWORD=""
# SENDINBLUE_LOGIN_URL="https://app.sendinblue.com/account/saml/login/truc"
# SMTP Provider: Mailjet
MAILJET_API_KEY=""
MAILJET_SECRET_KEY=""
# API Entreprise https://api.gouv.fr/api/api-entreprise.html
# External service: live chat for admins (specific to démarches-simplifiées.fr)
CRISP_ENABLED="disabled"
CRISP_CLIENT_KEY=""
# External service: mail catcher for staging environments (specific to démarches-simplifiées.fr)
MAILTRAP_ENABLED="disabled"
MAILTRAP_USERNAME=""
MAILTRAP_PASSWORD=""
# API Entreprise credentials
# https://api.gouv.fr/api/api-entreprise.html
API_ENTREPRISE_KEY=""
# Service externe: CRM de suivi de création d'administrateur | Spécifique démarches-simplifiées.fr
# External service: CRM for following admin accounts pipeline (specific to démarches-simplifiées.fr)
PIPEDRIVE_KEY=""
# Liste des réseaux qui passent outre la génération de token pour identifier un device, ainsi que le throttling par rack-attack
# Networks bypassing the email login token that verifies new devices, and rack-attack throttling
TRUSTED_NETWORKS=""
# Service externe: mesure de performance d'appli Rails | Spécifique démarches-simplifiées.fr
# External service: mesuring performance of the Rails app (specific to démarches-simplifiées.fr)
SKYLIGHT_AUTHENTICATION_KEY=""
# Activer ou non les logs LogRage
# Enable or disable Lograge logs
LOGRAGE_ENABLED="disabled"
# Service externe d'horodatage des changements de statut des dossiers (effectué quotidiennement)
# Logs source for Lograge
#
# Examples:
# * For local development: tps_local
# * For preproduction: tps_staging
# * For production: tps_prod
LOGRAGE_SOURCE="tps_local"
# External service: timestamping a daily archive of dossiers status changes
UNIVERSIGN_API_URL="https://ws.universign.eu/tsa/post/"
UNIVERSIGN_USERPWD=""
# API Geo / Adresse
# External service: API Geo / Adresse
API_ADRESSE_URL="https://api-adresse.data.gouv.fr"
API_GEO_URL="https://geo.api.gouv.fr"
# API Education
# External service: API Education
API_EDUCATION_URL="https://data.education.gouv.fr/api/records/1.0"
# Modifier le nb de tentatives de relance de job si echec
# MAX_ATTEMPTS_JOBS=25
# MAX_ATTEMPTS_API_ENTREPRISE_JOBS=5
# Clé de chriffrement des données sensibles en base
# Encryption key for sensitive columns in the database
ENCRYPTION_SERVICE_SALT=""
# Salt for invisible_captcha session data.
# Must be the same value for all app instances behind a load-balancer.
INVISIBLE_CAPTCHA_SECRET="kikooloool"

View file

@ -1,24 +1,28 @@
# Optional environment variables
# Variables d'environnement optionnelles
# Les paramètres pour l'affichage du nom l'application, et pour la génération des liens
# Application name, for display and generating links
APPLICATION_NAME="demarches-simplifiees.fr"
APPLICATION_SHORTNAME="d-s.fr"
APPLICATION_BASE_URL="https://www.demarches-simplifiees.fr"
# If defined to "staging", tell the app that it's running on a staging instance
DS_ENV="staging"
# Utilisation de France Connect
# FRANCE_CONNECT_ENABLED="disabled" # "enabled" par défaut
# France Connect usage
# FRANCE_CONNECT_ENABLED="disabled" # "enabled" by default
# Utilisation de Agent Connect
# AGENT_CONNECT_ENABLED="disabled" # "enabled" par défaut
# Agent Connect usage
# AGENT_CONNECT_ENABLED="disabled" # "enabled" by default
# Personnalisation d'instance - URLs des CGU et des mentions légales
# Configure the maximum number of times a job is retried
# MAX_ATTEMPTS_JOBS=25
# MAX_ATTEMPTS_API_ENTREPRISE_JOBS=5
# Instance customization: URLs for GTS and legal mentions
# CGU_URL=""
# MENTIONS_LEGALES_URL=""
# Personnalisation d'instance - Adresses Email de l'application et téléphone
# Instance customization: support emails addresses and phone
# CONTACT_EMAIL=""
# EQUIPE_EMAIL=""
# TECH_EMAIL=""
@ -26,61 +30,62 @@ DS_ENV="staging"
# OLD_CONTACT_EMAIL=""
# CONTACT_PHONE=""
# Personnalisation d'instance - Adresses postale de l'opérateur de l'instance
# Instance customization: postal address of the instance operator
# CONTACT_ADDRESS="Incubateur de Services Numériques / beta.gouv.fr\nServices du Premier Ministre, 20 avenue de Ségur, 75007 Paris"
# Personnalisation d'instance - URL pour la création de compte administrateur sur l'instance
# Instance customization: URL for creating an admin account on the instance
# DEMANDE_INSCRIPTION_ADMIN_PAGE_URL=""
# Personnalisation d'instance - URL du site web de documentation
# Instance customization: URL of the documentation website
# DOC_URL="https://doc.demarches-simplifiees.fr"
# Personnalisation d'instance - URL du site web FAQ
# Instance customization: URL of the documentation support website
# FAQ_URL="https://faq.demarches-simplifiees.fr"
# Personnalisation d'instance - URL de la déclaration d'accessibilité
# Instance customization: URL of the accessibility statement
# ACCESSIBILITE_URL=""
# Personnalisation d'instance - Page externe "Disponibilité" (status page)
# Instance customization: URL of the availability/status webpage
# STATUS_PAGE_URL=""
# Personnalisation d'instance - Favicons ---> à placer dans "app/assets/images"
# Instance customization: Favicons ---> to be put in "app/assets/images"
# FAVICON_16PX_SRC="favicons/16x16.png"
# FAVICON_32PX_SRC="favicons/32x32.png"
# FAVICON_96PX_SRC="favicons/96x96.png"
# Personnalisation d'instance - Logo de l'application ---> à placer dans "app/assets/images"
# Instance customization: Application logo ---> to be put in "app/assets/images"
# HEADER_LOGO_SRC="marianne.png"
# HEADER_LOGO_ALT=""
# HEADER_LOGO_WIDTH="65"
# HEADER_LOGO_HEIGHT="56"
# Personnalisation d'instance - Logo dans l'entête des emails ---> à placer dans "app/assets/images"
# Instance customization: Emails header logo ---> to be put in "app/assets/images"
# MAILER_LOGO_SRC="mailer/instructeur_mailer/logo.png"
# Personnalisation d'instance - Logo dans le pied de page des emails ---> à placer dans "app/assets/images"
# Instance customization: Emails footer logo ---> to be put in "app/assets/images"
# MAILER_FOOTER_LOGO_SRC="mailer/instructeur_mailer/logo-beta-gouv-fr.png"
# Personnalisation d'instance - Logo par défaut d'une procédure ---> à placer dans "app/assets/images"
# Instance customization: Procedure default logo ---> to be put in "app/assets/images"
# PROCEDURE_DEFAULT_LOGO_SRC="republique-francaise-logo.svg"
# Personnalisation d'instance - Logo dans le PDF d'export d'un dossier ---> à placer dans "app/assets/images"
# Instance customization: PDF export logo ---> to be put in "app/assets/images"
# DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.svg"
# Personnalisation d'instance - fichier utilisé pour poser un filigrane sur les pièces d'identité
# Instance customization: watermark for identity documents
# WATERMARK_FILE=""
# Active le mode maintenance
# Enabling maintenance mode
# MAINTENANCE_MODE="true"
# Active la localisation
# Enabling localization
# LOCALIZATION_ENABLED="true"
# Désactivé l'OTP pour SuperAdmin
# Disabling 2FA for Super-Admins
# SUPER_ADMIN_OTP_ENABLED = "disabled" # "enabled" par défaut
# API Particulier https://api.gouv.fr/les-api/api-particulier
# API Particulier
# https://api.gouv.fr/les-api/api-particulier
# API_PARTICULIER_URL="https://particulier.api.gouv.fr/api"
# Les instructeurs et administrateurs peuvent changer leur email vers ces domaines
# Admins and instructeurs can freely change their email to these domains
# LEGIT_ADMIN_DOMAINS = "domaine_1.com;domaine_2.com"

View file

@ -1,11 +0,0 @@
fr:
dossier_mailer:
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 ladministration.
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 <b>« %{libelle_demarche} »</b> est <b>supprimé</b>.<br><br>
Cette suppression ne modifie pas le statut final (accepté, refusé ou sans suite) de votre dossier.<br><br>
Une trace de ce dossier est visible dans votre interface : <a href='%{deleted_dossiers_link}'>%{deleted_dossiers_link}</a>.

View file

@ -0,0 +1,5 @@
fr:
dossier_mailer:
notify_en_construction_deletion_to_administration:
subject: Le dossier nº %{dossier_id} a été supprimé à la demande de lusager
body: À la demande de lusager, le dossier n° %{dossier_id} (%{procedure}) a été supprimé.

View file

@ -4,7 +4,7 @@ fr:
subject:
one: Une demande de transfert de dossier vous est adressée
other: Une demande de transfert de dossiers vous est adressée
transfer_link: demande de transfer
transfer_link: demande de transfert
body:
one: |
Accéder à la demande de transfert du dossier en cliquant sur le lien suivant :

View file

@ -12,7 +12,6 @@
defaults: &defaults
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
encryption_service_salt: <%= ENV["ENCRYPTION_SERVICE_SALT"] %>
signing_key: <%= ENV["SIGNING_KEY"] %>
otp_secret_key: <%= ENV["OTP_SECRET_KEY"] %>
basic_auth:
username: <%= ENV['BASIC_AUTH_USERNAME'] %>
@ -25,7 +24,7 @@ defaults: &defaults
token_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/token
userinfo_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/userinfo
logout_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/logout
agent_connect:
agent_connect:
identifier: <%= ENV['AGENT_CONNECT_ID'] %>
secret: <%= ENV['AGENT_CONNECT_SECRET'] %>
redirect_uri: <%= ENV['AGENT_CONNECT_REDIRECT'] %>
@ -56,6 +55,7 @@ defaults: &defaults
api_v3_key: <%= ENV['SENDINBLUE_API_V3_KEY'] %>
matomo:
enabled: <%= ENV['MATOMO_ENABLED'] == 'enabled' %>
host: <%= ENV['MATOMO_HOST'] %>
client_key: <%= ENV['MATOMO_ID'] %>
sentry:
enabled: <%= ENV['SENTRY_ENABLED'] == 'enabled' %>
@ -85,7 +85,6 @@ test:
<<: *defaults
secret_key_base: aa52abc3f3a629d04a61e9899a24c12f52b24c679cbf45f8ec0cdcc64ab9526d673adca84212882dff3911ac98e0c32ec4729ca7b3429ba18ef4dfd1bd18bc7a
encryption_service_salt: QUDyMoXyw2YXU8pHnpts3w9MyMpsMQ6BgP62obgCf7PQv
signing_key: aef3153a9829fa4ba10acb02927ac855df6b92795b1ad265d654443c4b14a017
otp_secret_key: 78ddda3679dc0ba2c99f50bcff04f49d862358dbeb7ead50368fdd6de14392be884ee10a204a0375b4b382e1a842fafe40d7858b7ab4796ec3a67c518d31112b
api_entreprise:
key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik9oIHllYWgiLCJpYXQiOjE1MTYyMzkwMjJ9.f06sBo3q2Yxnw_TYPFUEs0CozBmcV-XniH_DeKNWzKE"

View file

@ -0,0 +1,5 @@
class AddAttestationTemplateIdToProcedureRevisions < ActiveRecord::Migration[6.1]
def change
add_reference :procedure_revisions, :attestation_template, foreign_key: { to_table: :attestation_templates }, null: true, index: true
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_12_02_133139) do
ActiveRecord::Schema.define(version: 2022_01_10_133139) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -593,6 +593,8 @@ ActiveRecord::Schema.define(version: 2021_12_02_133139) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "published_at"
t.bigint "attestation_template_id"
t.index ["attestation_template_id"], name: "index_procedure_revisions_on_attestation_template_id"
t.index ["procedure_id"], name: "index_procedure_revisions_on_procedure_id"
end
@ -872,6 +874,7 @@ ActiveRecord::Schema.define(version: 2021_12_02_133139) do
add_foreign_key "procedure_revision_types_de_champ", "procedure_revision_types_de_champ", column: "parent_id"
add_foreign_key "procedure_revision_types_de_champ", "procedure_revisions", column: "revision_id"
add_foreign_key "procedure_revision_types_de_champ", "types_de_champ"
add_foreign_key "procedure_revisions", "attestation_templates"
add_foreign_key "procedure_revisions", "procedures"
add_foreign_key "procedures", "procedure_revisions", column: "draft_revision_id"
add_foreign_key "procedures", "procedure_revisions", column: "published_revision_id"

View file

@ -0,0 +1,23 @@
namespace :after_party do
desc 'Deployment task: moving Attestations from Procedure to ProcedureRevision'
task revise_attestation_templates: :environment do
rake_puts "Running deploy task 'revise_attestation_templates'"
attestation_templates = AttestationTemplate.where.not(procedure_id: nil)
progress = ProgressReport.new(attestation_templates.count)
attestation_templates.find_each do |attestation_template|
ProcedureRevision
.where(procedure_id: attestation_template.procedure_id, attestation_template_id: nil)
.update_all(attestation_template_id: attestation_template)
attestation_template.update_column(:procedure_id, nil)
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -1,8 +1,8 @@
include ActionDispatch::TestProcess
describe Administrateurs::AttestationTemplatesController, type: :controller do
let!(:attestation_template) { create(:attestation_template) }
let(:admin) { create(:administrateur) }
let(:attestation_template) { build(:attestation_template) }
let!(:procedure) { create :procedure, administrateur: admin, attestation_template: attestation_template }
let(:logo) { fixture_file_upload('spec/fixtures/files/white.png', 'image/png') }
let(:logo2) { fixture_file_upload('spec/fixtures/files/white.png', 'image/png') }
@ -41,7 +41,7 @@ describe Administrateurs::AttestationTemplatesController, type: :controller do
end
context 'if an attestation template exists on the procedure' do
after { procedure.attestation_template.destroy }
after { procedure.draft_revision.attestation_template&.destroy }
context 'with images' do
let!(:attestation_template) do
@ -115,14 +115,14 @@ describe Administrateurs::AttestationTemplatesController, type: :controller do
procedure.reload
end
it { expect(procedure.attestation_template).to have_attributes(attestation_params) }
it { expect(procedure.attestation_template.activated).to be true }
it { expect(procedure.attestation_template.logo.download).to eq(logo2.read) }
it { expect(procedure.attestation_template.signature.download).to eq(signature2.read) }
it { expect(procedure.draft_attestation_template).to have_attributes(attestation_params) }
it { expect(procedure.draft_attestation_template.activated).to be true }
it { expect(procedure.draft_attestation_template.logo.download).to eq(logo2.read) }
it { expect(procedure.draft_attestation_template.signature.download).to eq(signature2.read) }
it { expect(response).to redirect_to edit_admin_procedure_attestation_template_path(procedure) }
it { expect(flash.notice).to eq("L'attestation a bien été sauvegardée") }
after { procedure.attestation_template.destroy }
after { procedure.draft_attestation_template.destroy }
end
context 'when something wrong happens in the attestation template creation' do
@ -140,7 +140,7 @@ describe Administrateurs::AttestationTemplatesController, type: :controller do
it { expect(response).to redirect_to edit_admin_procedure_attestation_template_path(procedure) }
it { expect(flash.alert).to be_present }
it { expect(procedure.attestation_template).to be nil }
it { expect(procedure.draft_attestation_template).to be nil }
end
end
@ -158,13 +158,13 @@ describe Administrateurs::AttestationTemplatesController, type: :controller do
procedure.reload
end
it { expect(procedure.attestation_template).to have_attributes(attestation_params) }
it { expect(procedure.attestation_template.logo.download).to eq(logo2.read) }
it { expect(procedure.attestation_template.signature.download).to eq(signature2.read) }
it { expect(procedure.draft_attestation_template).to have_attributes(attestation_params) }
it { expect(procedure.draft_attestation_template.logo.download).to eq(logo2.read) }
it { expect(procedure.draft_attestation_template.signature.download).to eq(signature2.read) }
it { expect(response).to redirect_to edit_admin_procedure_attestation_template_path(procedure) }
it { expect(flash.notice).to eq("L'attestation a bien été modifiée") }
after { procedure.attestation_template.destroy }
after { procedure.draft_attestation_template&.destroy }
end
context 'when something wrong happens in the attestation template creation' do

View file

@ -411,9 +411,21 @@ describe Administrateurs::ProceduresController, type: :controller do
context 'when dossier is en_construction' do
let(:dossier) { create(:dossier, :en_construction, procedure: procedure_published) }
it { expect(procedure.reload.close?).to be_truthy }
it { expect(procedure.reload.discarded?).to be_truthy }
it { expect(dossier.reload.discarded?).to be_truthy }
it do
expect(procedure.reload.close?).to be_truthy
expect(procedure.discarded?).to be_truthy
expect(dossier.reload.kept?).to be_truthy
end
end
context 'when dossier is accepte' do
let(:dossier) { create(:dossier, :accepte, procedure: procedure_published) }
it do
expect(procedure.reload.close?).to be_truthy
expect(procedure.discarded?).to be_truthy
expect(dossier.reload.hidden_by_administration?).to be_truthy
end
end
end
@ -427,8 +439,19 @@ describe Administrateurs::ProceduresController, type: :controller do
context 'when dossier is en_construction' do
let(:dossier) { create(:dossier, :en_construction, procedure: procedure_published) }
it { expect(procedure.reload.discarded?).to be_truthy }
it { expect(dossier.reload.discarded?).to be_truthy }
it do
expect(procedure.reload.discarded?).to be_truthy
expect(dossier.reload.kept?).to be_truthy
end
end
context 'when dossier is accepte' do
let(:dossier) { create(:dossier, :accepte, procedure: procedure_published) }
it do
expect(procedure.reload.discarded?).to be_truthy
expect(dossier.reload.hidden_by_administration?).to be_truthy
end
end
end

View file

@ -766,7 +766,6 @@ describe Instructeurs::DossiersController, type: :controller do
before do
dossier.accepter!(instructeur: instructeur, motivation: "le dossier est correct")
dossier.update!(hidden_by_user_at: Time.zone.now.beginning_of_day.utc)
allow(DossierMailer).to receive(:notify_instructeur_deletion_to_user).and_return(double(deliver_later: nil))
subject
end
@ -775,10 +774,6 @@ describe Instructeurs::DossiersController, type: :controller do
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)
@ -794,7 +789,6 @@ describe Instructeurs::DossiersController, type: :controller do
context 'when the instructeur want to delete a dossier with a decision and not hidden by user' do
before do
dossier.accepter!(instructeur: instructeur, motivation: "le dossier est correct")
allow(DossierMailer).to receive(:notify_instructeur_deletion_to_user).and_return(double(deliver_later: nil))
subject
end
@ -803,10 +797,6 @@ describe Instructeurs::DossiersController, type: :controller do
expect(DossierOperationLog.where(dossier_id: dossier.id).last.operation).not_to eq('supprimer')
end
it 'does not send an email to the user' do
expect(DossierMailer).not_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(0)
end

View file

@ -107,6 +107,7 @@ describe Instructeurs::ProceduresController, type: :controller do
before do
create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_construction))
create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_construction), hidden_by_user_at: 1.hour.ago)
create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_instruction))
create(:dossier, procedure: procedure, state: Dossier.states.fetch(:sans_suite), archived: true)
@ -238,13 +239,26 @@ describe Instructeurs::ProceduresController, type: :controller do
let!(:new_unfollow_dossier) { create(:dossier, :en_instruction, procedure: procedure) }
before { subject }
it { expect(assigns(:a_suivre_dossiers)).to match_array([new_unfollow_dossier]) }
it { expect(assigns(:followed_dossiers)).to be_empty }
it { expect(assigns(:termines_dossiers)).to be_empty }
it { expect(assigns(:all_state_dossiers)).to match_array([new_unfollow_dossier]) }
it { expect(assigns(:archived_dossiers)).to be_empty }
context 'with a dossier en contruction hidden by user' do
let!(:hidden_dossier) { create(:dossier, groupe_instructeur: gi_2, state: Dossier.states.fetch(:en_construction), hidden_by_user_at: 1.hour.ago) }
before { subject }
it { expect(assigns(:all_state_dossiers)).to match_array([new_unfollow_dossier]) }
end
context 'with a dossier en contruction not hidden by user' do
let!(:en_construction_dossier) { create(:dossier, groupe_instructeur: gi_2, state: Dossier.states.fetch(:en_construction)) }
before { subject }
it { expect(assigns(:all_state_dossiers)).to match_array([new_unfollow_dossier, en_construction_dossier]) }
end
context 'and dossiers without follower on each of the others groups' do
let!(:new_unfollow_dossier_on_gi_2) { create(:dossier, groupe_instructeur: gi_2, state: Dossier.states.fetch(:en_instruction)) }
let!(:new_unfollow_dossier_on_gi_3) { create(:dossier, groupe_instructeur: gi_3, state: Dossier.states.fetch(:en_instruction)) }

View file

@ -1,47 +0,0 @@
describe Manager::DossiersController, type: :controller do
let(:super_admin) { create :super_admin }
let(:deleted_dossier) { DeletedDossier.find_by(dossier_id: dossier) }
let(:operations) { dossier.dossier_operation_logs.map(&:operation).map(&:to_sym) }
before { sign_in super_admin }
describe '#discard' do
let(:dossier) { create(:dossier, :en_construction) }
before do
post :discard, params: { id: dossier.id }
dossier.reload
end
it { expect(dossier.discarded?).to be_truthy }
it { expect(deleted_dossier).not_to be_nil }
it { expect(deleted_dossier.reason).to eq("manager_request") }
it { expect(operations).to eq([:supprimer]) }
end
describe '#restore' do
let(:dossier) { create(:dossier, :en_construction, :with_individual) }
before do
dossier.discard_and_keep_track!(super_admin, :manager_request)
post :restore, params: { id: dossier.id }
dossier.reload
end
it { expect(dossier.kept?).to be_truthy }
it { expect(deleted_dossier).to be_nil }
it { expect(operations).to eq([:supprimer, :restaurer]) }
end
describe '#repasser_en_instruction' do
let(:dossier) { create(:dossier, :accepte) }
before do
post :repasser_en_instruction, params: { id: dossier.id }
dossier.reload
end
it { expect(dossier.en_instruction?).to be_truthy }
end
end

View file

@ -27,10 +27,8 @@ describe Manager::ProceduresController, type: :controller do
end
describe '#discard' do
let(:dossier) { create(:dossier, :en_construction) }
let(:dossier) { create(:dossier, :accepte) }
let(:procedure) { dossier.procedure }
let(:deleted_dossier) { DeletedDossier.find_by(dossier_id: dossier.id) }
let(:operations) { dossier.dossier_operation_logs.map(&:operation).map(&:to_sym) }
before do
post :discard, params: { id: procedure.id }
@ -39,17 +37,12 @@ describe Manager::ProceduresController, type: :controller do
end
it { expect(procedure.discarded?).to be_truthy }
it { expect(dossier.discarded?).to be_truthy }
it { expect(deleted_dossier).not_to be_nil }
it { expect(deleted_dossier.reason).to eq("procedure_removed") }
it { expect(operations).to eq([:supprimer]) }
it { expect(dossier.hidden_by_administration?).to be_truthy }
end
describe '#restore' do
let(:dossier) { create(:dossier, :en_construction, :with_individual) }
let(:dossier) { create(:dossier, :accepte, :with_individual) }
let(:procedure) { dossier.procedure }
let(:deleted_dossier) { DeletedDossier.find_by(dossier_id: dossier.id) }
let(:operations) { dossier.dossier_operation_logs.map(&:operation).map(&:to_sym) }
before do
procedure.discard_and_keep_track!(super_admin)
@ -59,9 +52,7 @@ describe Manager::ProceduresController, type: :controller do
end
it { expect(procedure.kept?).to be_truthy }
it { expect(dossier.kept?).to be_truthy }
it { expect(deleted_dossier).to be_nil }
it { expect(operations).to eq([:supprimer, :restaurer]) }
it { expect(dossier.hidden_by_administration?).to be_falsey }
end
describe '#index' do

View file

@ -1008,7 +1008,6 @@ describe Users::DossiersController, type: :controller do
shared_examples_for "the dossier can not be deleted" do
it "doesnt notify the deletion" do
expect(DossierMailer).not_to receive(:notify_deletion_to_administration)
expect(DossierMailer).not_to receive(:notify_deletion_to_user)
subject
end
@ -1024,7 +1023,6 @@ describe Users::DossiersController, type: :controller do
it "notifies the user and the admin of the deletion" do
expect(DossierMailer).to receive(:notify_deletion_to_administration).with(kind_of(DeletedDossier), dossier.procedure.administrateurs.first.email).and_return(double(deliver_later: nil))
expect(DossierMailer).to receive(:notify_deletion_to_user).with(kind_of(DeletedDossier), dossier.user.email).and_return(double(deliver_later: nil))
subject
end

View file

@ -4,8 +4,6 @@ FactoryBot.define do
body { 'body' }
footer { 'footer' }
activated { true }
association :procedure
end
trait :with_files do

View file

@ -23,10 +23,11 @@ FactoryBot.define do
types_de_champ { [] }
types_de_champ_private { [] }
updated_at { nil }
attestation_template { }
end
after(:build) do |procedure, evaluator|
initial_revision = build(:procedure_revision, procedure: procedure)
initial_revision = build(:procedure_revision, procedure: procedure, attestation_template: evaluator.attestation_template)
add_types_de_champs(evaluator.types_de_champ, to: initial_revision, scope: :public)
add_types_de_champs(evaluator.types_de_champ_private, to: initial_revision, scope: :private)

View file

@ -60,16 +60,11 @@ RSpec.describe DossierMailer, type: :mailer do
it { expect(subject.perform_deliveries).to be_falsy }
end
describe '.notify_deletion_to_user' do
let(:deleted_dossier) { build(:deleted_dossier) }
def notify_deletion_to_administration(deleted_dossier, to_email)
@subject = default_i18n_subject(dossier_id: deleted_dossier.dossier_id)
@deleted_dossier = deleted_dossier
subject { described_class.notify_deletion_to_user(deleted_dossier, to_email) }
it { expect(subject.subject).to eq("Votre dossier nº #{deleted_dossier.dossier_id} a bien été supprimé") }
it { expect(subject.body).to include("Votre dossier") }
it { expect(subject.body).to include(deleted_dossier.dossier_id) }
it { expect(subject.body).to include("a bien été supprimé") }
it { expect(subject.body).to include(deleted_dossier.procedure.libelle) }
mail(to: to_email, subject: @subject)
end
describe '.notify_deletion_to_administration' do

View file

@ -45,10 +45,6 @@ class DossierMailerPreview < ActionMailer::Preview
DossierMailer.notify_brouillon_deletion(dossier_hashes, usager_email)
end
def notify_deletion_to_user
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

View file

@ -799,7 +799,6 @@ describe Dossier do
let(:reason) { :user_request }
before do
allow(DossierMailer).to receive(:notify_deletion_to_user).and_return(double(deliver_later: nil))
allow(DossierMailer).to receive(:notify_deletion_to_administration).and_return(double(deliver_later: nil))
end
@ -835,10 +834,6 @@ describe Dossier do
expect(deleted_dossier.deleted_at).to be_present
end
it 'notifies the user' do
expect(DossierMailer).to have_received(:notify_deletion_to_user).with(deleted_dossier, dossier.user.email)
end
it 'records the operation in the log' do
expect(last_operation.operation).to eq("supprimer")
expect(last_operation.automatic_operation?).to be_falsey
@ -1327,29 +1322,31 @@ describe Dossier do
end
describe 'discarded_brouillon_expired and discarded_en_construction_expired' do
let(:super_admin) { create(:super_admin) }
let(:administrateur) { create(:administrateur) }
let(:user) { administrateur.user }
let(:reason) { DeletedDossier.reasons.fetch(:user_request) }
before do
create(:dossier)
create(:dossier, :en_construction)
create(:dossier).discard!
create(:dossier, :en_construction).discard!
create(:dossier, user: user)
create(:dossier, :en_construction, user: user)
create(:dossier, user: user).discard_and_keep_track!(user, reason)
create(:dossier, :en_construction, user: user).discard_and_keep_track!(user, reason)
Timecop.travel(2.months.ago) do
create(:dossier).discard!
create(:dossier, :en_construction).discard!
create(:dossier, user: user).discard_and_keep_track!(user, reason)
create(:dossier, :en_construction, user: user).discard_and_keep_track!(user, reason)
create(:dossier).procedure.discard_and_keep_track!(super_admin)
create(:dossier, :en_construction).procedure.discard_and_keep_track!(super_admin)
create(:dossier, user: user).procedure.discard_and_keep_track!(administrateur)
create(:dossier, :en_construction, user: user).procedure.discard_and_keep_track!(administrateur)
end
Timecop.travel(1.week.ago) do
create(:dossier).discard!
create(:dossier, :en_construction).discard!
create(:dossier, user: user).discard_and_keep_track!(user, reason)
create(:dossier, :en_construction, user: user).discard_and_keep_track!(user, reason)
end
end
it { expect(Dossier.discarded_brouillon_expired.count).to eq(3) }
it { expect(Dossier.discarded_en_construction_expired.count).to eq(3) }
it { expect(Dossier.discarded_brouillon_expired.count).to eq(2) }
it { expect(Dossier.discarded_en_construction_expired.count).to eq(0) }
end
describe "discarded procedure dossier should be able to access it's procedure" do

View file

@ -169,7 +169,7 @@ describe ProcedureRevision do
expect(new_revision.revision_types_de_champ.last.position).to eq(2)
expect(new_revision.revision_types_de_champ.last.type_de_champ).to eq(new_type_de_champ)
expect(new_revision.revision_types_de_champ.last.type_de_champ.revision).to eq(new_revision)
expect(procedure.active_revision.changed?(new_revision)).to be_truthy
expect(procedure.active_revision.different_from?(new_revision)).to be_truthy
expect(procedure.active_revision.compare(new_revision)).to eq([
{
op: :add,

View file

@ -66,16 +66,12 @@ describe Procedure do
end
describe '#closed_mail_template_attestation_inconsistency_state' do
let(:procedure_without_attestation) { create(:procedure, closed_mail: closed_mail) }
let(:procedure_without_attestation) { create(:procedure, closed_mail: closed_mail, attestation_template: nil) }
let(:procedure_with_active_attestation) do
procedure = create(:procedure, closed_mail: closed_mail)
create(:attestation_template, procedure: procedure, activated: true)
procedure
create(:procedure, closed_mail: closed_mail, attestation_template: build(:attestation_template, activated: true))
end
let(:procedure_with_inactive_attestation) do
procedure = create(:procedure, closed_mail: closed_mail)
create(:attestation_template, procedure: procedure, activated: false)
procedure
create(:procedure, closed_mail: closed_mail, attestation_template: build(:attestation_template, activated: false))
end
subject { procedure.closed_mail_template_attestation_inconsistency_state }
@ -272,6 +268,16 @@ describe Procedure do
let(:procedure) { build(:procedure, monavis_embed: monavis_issue_phillipe) }
it { expect(procedure.valid?).to eq(true) }
end
context 'Monavis embed code without title allowed' do
monavis_issue_bouchra = <<-MSG
<a href="https://voxusagers.numerique.gouv.fr/Demarches/3193?&view-mode=formulaire-avis&nd_mode=en-ligne-enti%C3%A8rement&nd_source=button&key=58e099a09c02abe629c14905ed2b055d">
<img src="https://voxusagers.numerique.gouv.fr/static/bouton-bleu.svg" alt="Je donne mon avis" />
</a>
MSG
let(:procedure) { build(:procedure, monavis_embed: monavis_issue_bouchra) }
it { expect(procedure.valid?).to eq(true) }
end
end
shared_examples 'duree de conservation' do

View file

@ -416,5 +416,26 @@ describe ExpiredDossiersDeletionService do
it { expect(DossierMailer).to have_received(:notify_automatic_deletion_to_administration).with([deleted_dossier_1], dossier_1.procedure.administrateurs.first.email) }
it { expect(DossierMailer).to have_received(:notify_automatic_deletion_to_administration).with([deleted_dossier_2], dossier_2.procedure.administrateurs.first.email) }
end
context 'with 1 dossier deleted by user and 1 dossier deleted by administration' do
let!(:dossier_1) { create(:dossier, :accepte, procedure: procedure, user: user, hidden_by_administration_at: 1.hour.ago, termine_close_to_expiration_notice_sent_at: (warning_period + 1.day).ago) }
let!(:dossier_2) { create(:dossier, :refuse, procedure: procedure_2, user: user, hidden_by_user_at: 1.hour.ago, termine_close_to_expiration_notice_sent_at: (warning_period + 1.day).ago) }
let(:deleted_dossier_1) { DeletedDossier.find_by(dossier_id: dossier_1.id) }
let(:deleted_dossier_2) { DeletedDossier.find_by(dossier_id: dossier_2.id) }
let!(:instructeur) { create(:instructeur) }
before do
instructeur.followed_dossiers << dossier_1 << dossier_2
ExpiredDossiersDeletionService.delete_expired_termine_and_notify
end
it { expect(DossierMailer).to have_received(:notify_automatic_deletion_to_user).once }
it { expect(DossierMailer).to have_received(:notify_automatic_deletion_to_user).with(match_array([deleted_dossier_1]), user.email) }
it { expect(DossierMailer).to have_received(:notify_automatic_deletion_to_administration).twice }
it { expect(DossierMailer).to have_received(:notify_automatic_deletion_to_administration).with(match_array([deleted_dossier_2]), instructeur.email) }
it { expect(DossierMailer).to have_received(:notify_automatic_deletion_to_administration).with([deleted_dossier_2], dossier_2.procedure.administrateurs.first.email) }
end
end
end

View file

@ -5,6 +5,11 @@ describe ProcedureArchiveService do
let(:year) { 2020 }
let(:month) { 3 }
let(:date_month) { Date.strptime("#{year}-#{month}", "%Y-%m") }
let(:groupe_instructeurs) { instructeur.groupe_instructeurs }
before do
procedure.defaut_groupe_instructeur.add(instructeur)
end
describe '#create_pending_archive' do
context 'for a specific month' do
@ -37,7 +42,7 @@ describe ProcedureArchiveService do
after { Timecop.return }
context 'for a specific month' do
let(:archive) { create(:archive, time_span_type: 'monthly', status: 'pending', month: date_month) }
let(:archive) { create(:archive, time_span_type: 'monthly', status: 'pending', month: date_month, groupe_instructeurs: groupe_instructeurs) }
let(:year) { 2021 }
let(:mailer) { double('mailer', deliver_later: true) }
@ -99,7 +104,7 @@ describe ProcedureArchiveService do
end
context 'for all months' do
let(:archive) { create(:archive, time_span_type: 'everything', status: 'pending') }
let(:archive) { create(:archive, time_span_type: 'everything', status: 'pending', groupe_instructeurs: groupe_instructeurs) }
let(:mailer) { double('mailer', deliver_later: true) }
it 'collect files' do
@ -125,7 +130,7 @@ describe ProcedureArchiveService do
after { Timecop.return }
context 'for a specific month' do
let(:archive) { create(:archive, time_span_type: 'monthly', status: 'pending', month: date_month) }
let(:archive) { create(:archive, time_span_type: 'monthly', status: 'pending', month: date_month, groupe_instructeurs: groupe_instructeurs) }
let(:year) { 2021 }
let(:mailer) { double('mailer', deliver_later: true) }
@ -228,7 +233,7 @@ describe ProcedureArchiveService do
end
context 'for all months' do
let(:archive) { create(:archive, time_span_type: 'everything', status: 'pending') }
let(:archive) { create(:archive, time_span_type: 'everything', status: 'pending', groupe_instructeurs: groupe_instructeurs) }
let(:mailer) { double('mailer', deliver_later: true) }
it 'collect files' do

View file

@ -105,8 +105,7 @@ module SystemHelpers
def select_combobox(libelle, fill_with, value, check: true)
fill_in libelle, with: fill_with
selector = "li[data-option-value=\"#{value}\"]"
find(selector).click
find('li[role="option"]', text: value).click
if check
check_selected_value(libelle, with: value)
end

View file

@ -165,8 +165,8 @@ describe 'Instructing a dossier:', js: true do
click_on 'Personnes impliquées'
select_combobox('Emails', instructeur_2.email, instructeur_2.id, check: false)
select_combobox('Emails', instructeur_3.email, instructeur_3.id, check: false)
select_combobox('Emails', instructeur_2.email, instructeur_2.email, check: false)
select_combobox('Emails', instructeur_3.email, instructeur_3.email, check: false)
click_on 'Envoyer'

View file

@ -32,7 +32,7 @@ describe "procedure filters" do
end
scenario "should add be able to add created_at column", js: true do
add_column("Créé le", "self/created_at")
add_column("Créé le")
within ".dossiers-table" do
expect(page).to have_link("Créé le")
expect(page).to have_link(new_unfollow_dossier.created_at.strftime('%d/%m/%Y'))
@ -40,7 +40,7 @@ describe "procedure filters" do
end
scenario "should add be able to add and remove custom type_de_champ column", js: true do
add_column(type_de_champ.libelle, "type_de_champ/#{type_de_champ.stable_id}")
add_column(type_de_champ.libelle)
within ".dossiers-table" do
expect(page).to have_link(type_de_champ.libelle)
expect(page).to have_link(champ.value)
@ -123,9 +123,9 @@ describe "procedure filters" do
click_button "Ajouter le filtre"
end
def add_column(column_name, column_path)
def add_column(column_name)
click_on 'Personnaliser'
select_combobox('Colonne à afficher', column_name, column_path, check: false)
select_combobox('Colonne à afficher', column_name, column_name, check: false)
click_button "Enregistrer"
end

View file

@ -1,5 +1,6 @@
describe 'admin/_closed_mail_template_attestation_inconsistency_alert.html.haml', type: :view do
let(:procedure) { create(:procedure, closed_mail: closed_mail) }
let(:procedure) { create(:procedure, closed_mail: closed_mail, attestation_template: attestation_template) }
let(:attestation_template) { nil }
def alert
assign(:procedure, procedure)
@ -23,7 +24,7 @@ describe 'admin/_closed_mail_template_attestation_inconsistency_alert.html.haml'
context 'when there is an active attestation but the closed mail template does not mention it' do
let(:closed_mail) { create(:closed_mail) }
let!(:attestation_template) { create(:attestation_template, procedure: procedure, activated: true) }
let(:attestation_template) { build(:attestation_template) }
it { expect(alert).to include("Cette démarche comporte une attestation, mais laccusé dacceptation ne la mentionne pas") }
it { expect(alert).to include(edit_admin_procedure_mail_template_path(procedure, Mails::ClosedMail::SLUG)) }

View file

@ -8703,9 +8703,9 @@ nano-time@1.0.0:
big-integer "^1.6.16"
nanoid@^3.1.30:
version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
version "3.2.0"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
nanomatch@^1.2.9:
version "1.2.13"