diff --git a/app/assets/stylesheets/password_complexity.scss b/app/assets/stylesheets/password_complexity.scss
index 24fc2ef77..ee521b0b3 100644
--- a/app/assets/stylesheets/password_complexity.scss
+++ b/app/assets/stylesheets/password_complexity.scss
@@ -9,12 +9,10 @@ $complexity-color-3: #FFD000;
$complexity-color-4: $green;
.password-complexity {
- margin-top: -24px;
width: 100%;
height: 12px;
background: $complexity-bg;
display: block;
- margin-bottom: $default-spacer;
text-align: center;
border-radius: 8px;
diff --git a/app/assets/stylesheets/procedure_context.scss b/app/assets/stylesheets/procedure_context.scss
index ee5a96010..e2f220fd3 100644
--- a/app/assets/stylesheets/procedure_context.scss
+++ b/app/assets/stylesheets/procedure_context.scss
@@ -100,7 +100,7 @@ $procedure-description-line-height: 22px;
// If the text exceeds the max-height,
// truncate it and displays the "Read more" button.
&.read-more-enabled {
- overflow: hidden;
+ overflow: auto;
border-bottom: 1px solid $border-grey;
+ .read-more-button {
diff --git a/app/components/dsfr/input_component.rb b/app/components/dsfr/input_component.rb
index aa724a775..271762470 100644
--- a/app/components/dsfr/input_component.rb
+++ b/app/components/dsfr/input_component.rb
@@ -6,7 +6,7 @@ class Dsfr::InputComponent < ApplicationComponent
# it uses aria-describedby on input and link it to yielded content
renders_one :describedby
- def initialize(form:, attribute:, input_type:, opts: {}, required: true)
+ def initialize(form:, attribute:, input_type: :text_field, opts: {}, required: true)
@form = form
@attribute = attribute
@input_type = input_type
@@ -40,19 +40,21 @@ class Dsfr::InputComponent < ApplicationComponent
'fr-mb-0': true,
'fr-input--error': errors_on_attribute?))
- if errors_on_attribute? || describedby
- @opts = @opts.deep_merge(aria: {
- describedby: error_message_id,
- invalid: errors_on_attribute?
+ if errors_on_attribute? || describedby?
+ @opts.deep_merge!(aria: {
+ describedby: describedby_id,
+ invalid: errors_on_attribute?
})
end
+
if @required
@opts[:required] = true
end
+
if email?
- @opts = @opts.deep_merge(data: {
+ @opts.deep_merge!(data: {
action: "blur->email-input#checkEmail",
- 'email-input-target': 'input'
+ 'email-input-target': 'input'
})
end
@opts
@@ -63,14 +65,14 @@ class Dsfr::InputComponent < ApplicationComponent
errors.has_key?(attribute_or_rich_body)
end
- def error_message_id
- dom_id(object, @attribute)
- end
-
def error_messages
errors.full_messages_for(attribute_or_rich_body)
end
+ def describedby_id
+ dom_id(object, "#{@attribute}-messages")
+ end
+
# i18n lookups
def label
object.class.human_attribute_name(@attribute)
@@ -89,6 +91,10 @@ class Dsfr::InputComponent < ApplicationComponent
@input_type == :email_field
end
+ def show_password_id
+ dom_id(object, "#{@attribute}_show_password")
+ end
+
private
def hint?
diff --git a/app/components/dsfr/input_component/input_component.html.haml b/app/components/dsfr/input_component/input_component.html.haml
index a56c7fff8..4e52ec670 100644
--- a/app/components/dsfr/input_component/input_component.html.haml
+++ b/app/components/dsfr/input_component/input_component.html.haml
@@ -7,13 +7,13 @@
- if hint?
%span.fr-hint-text= hint
- = @form.send(@input_type, @attribute, input_opts)
+ = @form.public_send(@input_type, @attribute, input_opts)
- if errors_on_attribute?
- if error_messages.size == 1
- %p.fr-error-text{ id: error_message_id }= error_messages.first
+ %p.fr-error-text{ id: describedby_id }= error_messages.first
- else
- .fr-error-text{ id: error_message_id }
+ .fr-error-text{ id: describedby_id }
%ul.list-style-type-none.fr-pl-0
- error_messages.map do |error_message|
%li= error_message
@@ -23,8 +23,8 @@
- if password?
.fr-password__checkbox.fr-checkbox-group.fr-checkbox-group--sm
- %input#show_password{ "aria-label" => t('.show_password.aria_label'), type: "checkbox" }/
- %label.fr--password__checkbox.fr-label{ for: "show_password" }= t('.show_password.label')
+ %input{ id: show_password_id, "aria-label" => t('.show_password.aria_label'), type: "checkbox" }/
+ %label.fr--password__checkbox.fr-label{ for: show_password_id }= t('.show_password.label')
- if email?
.suspect-email.hidden{ data: { "email-input-target": 'ariaRegion'}, aria: { live: 'off' } }
diff --git a/app/components/password_complexity_component.rb b/app/components/password_complexity_component.rb
new file mode 100644
index 000000000..32efd3908
--- /dev/null
+++ b/app/components/password_complexity_component.rb
@@ -0,0 +1,53 @@
+class PasswordComplexityComponent < ApplicationComponent
+ def initialize(length: nil, min_length: nil, score: nil, min_complexity: nil)
+ @length = length
+ @min_length = min_length
+ @score = score
+ @min_complexity = min_complexity
+ end
+
+ private
+
+ def filled?
+ !@length.nil? || !@score.nil?
+ end
+
+ def alert_classes
+ class_names(
+ "fr-alert": true,
+ "fr-alert--sm": true,
+ "fr-alert--info": !success?,
+ "fr-alert--success": success?
+ )
+ end
+
+ def success?
+ return false if !filled?
+
+ @length >= @min_length && @score >= @min_complexity
+ end
+
+ def complexity_classes
+ [
+ "password-complexity fr-mt-2w fr-mb-1w",
+ filled? ? "complexity-#{@length < @min_length ? @score / 2 : @score}" : nil
+ ]
+ end
+
+ def title
+ return t(".title.empty") if !filled?
+
+ return t(".title.too_short", min_length: @min_length) if @length < @min_length
+
+ case @score
+ when 0..1
+ return t(".title.weakest")
+ when 2...@min_complexity
+ return t(".title.weak")
+ when @min_complexity...4
+ return t(".title.passable")
+ else
+ return t(".title.strong")
+ end
+ end
+end
diff --git a/app/components/password_complexity_component/password_complexity_component.en.yml b/app/components/password_complexity_component/password_complexity_component.en.yml
new file mode 100644
index 000000000..d5930cd58
--- /dev/null
+++ b/app/components/password_complexity_component/password_complexity_component.en.yml
@@ -0,0 +1,10 @@
+---
+en:
+ title:
+ empty: Enter a password.
+ too_short: Password must be at least %{min_length} characters long.
+ passable: Password is acceptable. You can validate… or improve your password.
+ strong: Congratulations! Password is strong and secure enough.
+ weak: Vulnerable password.
+ weakest: Very vulnerable password.
+ hint: A short sentence with punctuation can be a very secure password.
diff --git a/app/components/password_complexity_component/password_complexity_component.fr.yml b/app/components/password_complexity_component/password_complexity_component.fr.yml
new file mode 100644
index 000000000..225a5775c
--- /dev/null
+++ b/app/components/password_complexity_component/password_complexity_component.fr.yml
@@ -0,0 +1,10 @@
+---
+fr:
+ title:
+ empty: Inscrivez un mot de passe.
+ too_short: Le mot de passe doit faire au moins %{min_length} caractères.
+ passable: Mot de passe acceptable. Vous pouvez valider… ou améliorer votre mot de passe.
+ strong: Félicitations ! Mot de passe suffisamment fort et sécurisé.
+ weak: Mot de passe vulnérable.
+ weakest: Mot de passe très vulnérable.
+ hint: Une courte phrase avec ponctuation peut être un mot de passe très sécurisé.
diff --git a/app/components/password_complexity_component/password_complexity_component.html.haml b/app/components/password_complexity_component/password_complexity_component.html.haml
new file mode 100644
index 000000000..bfa7d5620
--- /dev/null
+++ b/app/components/password_complexity_component/password_complexity_component.html.haml
@@ -0,0 +1,6 @@
+%div{ class: complexity_classes }
+
+%div{ class: alert_classes }
+ %h3.fr-alert__title= title
+ - if !success?
+ %p= t(".hint")
diff --git a/app/controllers/administrateurs/groupe_instructeurs_controller.rb b/app/controllers/administrateurs/groupe_instructeurs_controller.rb
index 63390b1f1..6ac2e3205 100644
--- a/app/controllers/administrateurs/groupe_instructeurs_controller.rb
+++ b/app/controllers/administrateurs/groupe_instructeurs_controller.rb
@@ -146,15 +146,18 @@ module Administrateurs
instructeur = groupe_instructeur.instructeurs.find_by(id: instructeur_id)
if groupe_instructeur.remove(instructeur)
- flash[:notice] = if procedure.routing_enabled?
- GroupeInstructeurMailer
- .remove_instructeurs(groupe_instructeur, [instructeur], current_administrateur.email)
- .deliver_later
-
+ flash[:notice] = if instructeur.in?(procedure.instructeurs)
"L’instructeur « #{instructeur.email} » a été retiré du groupe."
else
"L’instructeur a bien été désaffecté de la démarche"
end
+ GroupeInstructeurMailer
+ .notify_removed_instructeur(groupe_instructeur, instructeur, current_administrateur.email)
+ .deliver_later
+
+ GroupeInstructeurMailer
+ .notify_group_when_instructeurs_removed(groupe_instructeur, [instructeur], current_administrateur.email)
+ .deliver_later
else
flash[:alert] = if procedure.routing_enabled?
if instructeur.present?
diff --git a/app/controllers/administrateurs/procedures_controller.rb b/app/controllers/administrateurs/procedures_controller.rb
index c206b3724..e81a3a42c 100644
--- a/app/controllers/administrateurs/procedures_controller.rb
+++ b/app/controllers/administrateurs/procedures_controller.rb
@@ -96,8 +96,20 @@ module Administrateurs
@procedure = current_administrateur
.procedures
.includes(
- published_revision: :types_de_champ,
- draft_revision: :types_de_champ
+ published_revision: {
+ types_de_champ: [],
+ revision_types_de_champ: { type_de_champ: { piece_justificative_template_attachment: :blob } }
+ },
+ draft_revision: {
+ types_de_champ: [],
+ revision_types_de_champ: { type_de_champ: { piece_justificative_template_attachment: :blob } }
+ },
+ attestation_template: [],
+ initiated_mail: [],
+ received_mail: [],
+ closed_mail: [],
+ refused_mail: [],
+ without_continuation_mail: []
)
.find(params[:id])
@@ -332,7 +344,35 @@ module Administrateurs
end
def champs
- @procedure = Procedure.includes(draft_revision: { revision_types_de_champ_public: :type_de_champ }).find(@procedure.id)
+ @procedure = Procedure.includes(draft_revision: {
+ revision_types_de_champ: {
+ type_de_champ: { piece_justificative_template_attachment: :blob, revision: [], procedure: [] },
+ revision: [],
+ procedure: []
+ },
+ revision_types_de_champ_public: {
+ type_de_champ: { piece_justificative_template_attachment: :blob, revision: [], procedure: [] },
+ revision: [],
+ procedure: []
+ },
+ procedure: []
+ }).find(@procedure.id)
+ end
+
+ def annotations
+ @procedure = Procedure.includes(draft_revision: {
+ revision_types_de_champ: {
+ type_de_champ: { piece_justificative_template_attachment: :blob, revision: [], procedure: [] },
+ revision: [],
+ procedure: []
+ },
+ revision_types_de_champ_private: {
+ type_de_champ: { piece_justificative_template_attachment: :blob, revision: [], procedure: [] },
+ revision: [],
+ procedure: []
+ },
+ procedure: []
+ }).find(@procedure.id)
end
def detail
diff --git a/app/controllers/instructeurs/groupe_instructeurs_controller.rb b/app/controllers/instructeurs/groupe_instructeurs_controller.rb
index 6eb25e131..631b37afc 100644
--- a/app/controllers/instructeurs/groupe_instructeurs_controller.rb
+++ b/app/controllers/instructeurs/groupe_instructeurs_controller.rb
@@ -35,7 +35,11 @@ module Instructeurs
if groupe_instructeur.remove(instructeur)
flash[:notice] = "L’instructeur « #{instructeur.email} » a été retiré du groupe."
GroupeInstructeurMailer
- .remove_instructeurs(groupe_instructeur, [instructeur], current_user.email)
+ .notify_removed_instructeur(groupe_instructeur, instructeur, current_user.email)
+ .deliver_later
+
+ GroupeInstructeurMailer
+ .notify_group_when_instructeurs_removed(groupe_instructeur, [instructeur], current_user.email)
.deliver_later
else
flash[:alert] = "L’instructeur « #{instructeur.email} » n’est pas dans le groupe."
diff --git a/app/graphql/mutations/groupe_instructeur_supprimer_instructeurs.rb b/app/graphql/mutations/groupe_instructeur_supprimer_instructeurs.rb
index 864f0c3c3..0c1b1617d 100644
--- a/app/graphql/mutations/groupe_instructeur_supprimer_instructeurs.rb
+++ b/app/graphql/mutations/groupe_instructeur_supprimer_instructeurs.rb
@@ -17,7 +17,7 @@ module Mutations
if groupe_instructeur.procedure.routing_enabled? && instructeurs.present?
GroupeInstructeurMailer
- .remove_instructeurs(groupe_instructeur, instructeurs, current_administrateur.email)
+ .notify_group_when_instructeurs_removed(groupe_instructeur, instructeurs, current_administrateur.email)
.deliver_later
end
diff --git a/app/mailers/groupe_instructeur_mailer.rb b/app/mailers/groupe_instructeur_mailer.rb
index cb2865bb8..fa438399d 100644
--- a/app/mailers/groupe_instructeur_mailer.rb
+++ b/app/mailers/groupe_instructeur_mailer.rb
@@ -1,7 +1,7 @@
class GroupeInstructeurMailer < ApplicationMailer
layout 'mailers/layout'
- def remove_instructeurs(group, removed_instructeurs, current_instructeur_email)
+ def notify_group_when_instructeurs_removed(group, removed_instructeurs, current_instructeur_email)
@removed_instructeur_emails = removed_instructeurs.map(&:email)
@group = group
@current_instructeur_email = current_instructeur_email
@@ -11,4 +11,17 @@ class GroupeInstructeurMailer < ApplicationMailer
emails = @group.instructeurs.map(&:email)
mail(bcc: emails, subject: subject)
end
+
+ def notify_removed_instructeur(group, removed_instructeur, current_instructeur_email)
+ @group = group
+ @current_instructeur_email = current_instructeur_email
+ @still_assigned_to_procedure = removed_instructeur.in?(group.procedure.instructeurs)
+ subject = if @still_assigned_to_procedure
+ "Vous avez été retiré du groupe \"#{group.label}\" de la démarche \"#{group.procedure.libelle}\""
+ else
+ "Vous avez été désaffecté de la démarche \"#{group.procedure.libelle}\""
+ end
+
+ mail(to: removed_instructeur.email, subject: subject)
+ end
end
diff --git a/app/models/champs/multiple_drop_down_list_champ.rb b/app/models/champs/multiple_drop_down_list_champ.rb
index 365332e30..4e041fd0b 100644
--- a/app/models/champs/multiple_drop_down_list_champ.rb
+++ b/app/models/champs/multiple_drop_down_list_champ.rb
@@ -23,6 +23,8 @@
class Champs::MultipleDropDownListChamp < Champ
before_save :format_before_save
+ validate :values_are_in_options, if: -> { value.present? }
+
def options?
drop_down_list_options?
end
@@ -90,4 +92,12 @@ class Champs::MultipleDropDownListChamp < Champ
end
end
end
+
+ def values_are_in_options
+ json = selected_options.reject(&:blank?)
+ return if json.empty?
+ return if (json - enabled_non_empty_options).empty?
+
+ errors.add(:value, :not_in_options)
+ end
end
diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb
index 972882f5b..86f5f1100 100644
--- a/app/models/concerns/tags_substitution_concern.rb
+++ b/app/models/concerns/tags_substitution_concern.rb
@@ -270,12 +270,12 @@ module TagsSubstitutionConcern
end
def champ_public_tags(dossier: nil)
- types_de_champ = (dossier || procedure.active_revision).types_de_champ_public.not_condition
+ types_de_champ = (dossier || procedure.active_revision).types_de_champ_public.filter { !_1.condition? }
types_de_champ_tags(types_de_champ, Dossier::SOUMIS)
end
def champ_private_tags(dossier: nil)
- types_de_champ = (dossier || procedure.active_revision).types_de_champ_private.not_condition
+ types_de_champ = (dossier || procedure.active_revision).types_de_champ_private.filter { !_1.condition? }
types_de_champ_tags(types_de_champ, Dossier::INSTRUCTION_COMMENCEE)
end
diff --git a/app/models/prefill_params.rb b/app/models/prefill_params.rb
index f0841039a..4af3e8532 100644
--- a/app/models/prefill_params.rb
+++ b/app/models/prefill_params.rb
@@ -42,6 +42,7 @@ class PrefillParams
TypeDeChamp.type_champs.fetch(:pays),
TypeDeChamp.type_champs.fetch(:regions),
TypeDeChamp.type_champs.fetch(:departements),
+ TypeDeChamp.type_champs.fetch(:multiple_drop_down_list),
TypeDeChamp.type_champs.fetch(:epci)
]
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index b6ef52750..8e8515bd6 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -173,12 +173,15 @@ class Procedure < ApplicationRecord
types_de_champ_for_tags.private_only
end
- def revision_ids_with_pending_dossiers
- dossiers
- .where.not(revision_id: [draft_revision_id, published_revision_id].compact)
- .state_en_construction_ou_instruction
- .distinct(:revision_id)
- .pluck(:revision_id)
+ def revisions_with_pending_dossiers
+ @revisions_with_pending_dossiers ||= begin
+ ids = dossiers
+ .where.not(revision_id: [draft_revision_id, published_revision_id].compact)
+ .state_en_construction_ou_instruction
+ .distinct(:revision_id)
+ .pluck(:revision_id)
+ ProcedureRevision.includes(revision_types_de_champ: [:type_de_champ]).where(id: ids)
+ end
end
has_many :administrateurs_procedures, dependent: :delete_all
diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb
index 7e94d0595..ff3e587b5 100644
--- a/app/models/procedure_revision.rb
+++ b/app/models/procedure_revision.rb
@@ -132,7 +132,7 @@ class ProcedureRevision < ApplicationRecord
end
def draft?
- procedure.draft_revision == self
+ procedure.draft_revision_id == id
end
def locked?
@@ -172,11 +172,22 @@ class ProcedureRevision < ApplicationRecord
end
def children_of(tdc)
- parent_coordinate_id = revision_types_de_champ.where(type_de_champ: tdc).select(:id)
+ if revision_types_de_champ.loaded?
+ parent_coordinate_id = revision_types_de_champ
+ .filter { _1.type_de_champ_id == tdc.id }
+ .map(&:id)
- types_de_champ
- .where(procedure_revision_types_de_champ: { parent_id: parent_coordinate_id })
- .order("procedure_revision_types_de_champ.position")
+ revision_types_de_champ
+ .filter { _1.parent_id.in?(parent_coordinate_id) }
+ .sort_by(&:position)
+ .map(&:type_de_champ)
+ else
+ parent_coordinate_id = revision_types_de_champ.where(type_de_champ: tdc).select(:id)
+
+ types_de_champ
+ .where(procedure_revision_types_de_champ: { parent_id: parent_coordinate_id })
+ .order("procedure_revision_types_de_champ.position")
+ end
end
def remove_children_of(tdc)
@@ -380,7 +391,7 @@ class ProcedureRevision < ApplicationRecord
public_tdcs
.map.with_index
- .filter_map { |tdc, i| tdc.condition.present? ? [tdc, i] : nil }
+ .filter_map { |tdc, i| tdc.condition? ? [tdc, i] : nil }
.map { |tdc, i| [tdc, tdc.condition.errors(public_tdcs.take(i))] }
.filter { |_tdc, errors| errors.present? }
.each { |tdc, message| errors.add(:condition, message, type_de_champ: tdc) }
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index 0d9692fdc..354711ef2 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -262,13 +262,14 @@ class TypeDeChamp < ApplicationRecord
TypeDeChamp.type_champs.fetch(:iban),
TypeDeChamp.type_champs.fetch(:civilite),
TypeDeChamp.type_champs.fetch(:pays),
+ TypeDeChamp.type_champs.fetch(:regions),
TypeDeChamp.type_champs.fetch(:date),
TypeDeChamp.type_champs.fetch(:datetime),
TypeDeChamp.type_champs.fetch(:yes_no),
TypeDeChamp.type_champs.fetch(:checkbox),
TypeDeChamp.type_champs.fetch(:drop_down_list),
- TypeDeChamp.type_champs.fetch(:regions),
TypeDeChamp.type_champs.fetch(:departements),
+ TypeDeChamp.type_champs.fetch(:multiple_drop_down_list),
TypeDeChamp.type_champs.fetch(:epci)
])
end
diff --git a/app/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ.rb b/app/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ.rb
new file mode 100644
index 000000000..11992f53d
--- /dev/null
+++ b/app/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ.rb
@@ -0,0 +1,8 @@
+class TypesDeChamp::PrefillMultipleDropDownListTypeDeChamp < TypesDeChamp::PrefillDropDownListTypeDeChamp
+ def example_value
+ return nil if possible_values.empty?
+ return possible_values.first if possible_values.one?
+
+ [possible_values.first, possible_values.second]
+ end
+end
diff --git a/app/models/types_de_champ/prefill_type_de_champ.rb b/app/models/types_de_champ/prefill_type_de_champ.rb
index a385ea653..1dc57e46f 100644
--- a/app/models/types_de_champ/prefill_type_de_champ.rb
+++ b/app/models/types_de_champ/prefill_type_de_champ.rb
@@ -5,6 +5,8 @@ class TypesDeChamp::PrefillTypeDeChamp < SimpleDelegator
case type_de_champ.type_champ
when TypeDeChamp.type_champs.fetch(:drop_down_list)
TypesDeChamp::PrefillDropDownListTypeDeChamp.new(type_de_champ)
+ when TypeDeChamp.type_champs.fetch(:multiple_drop_down_list)
+ TypesDeChamp::PrefillMultipleDropDownListTypeDeChamp.new(type_de_champ)
when TypeDeChamp.type_champs.fetch(:pays)
TypesDeChamp::PrefillPaysTypeDeChamp.new(type_de_champ)
when TypeDeChamp.type_champs.fetch(:regions)
diff --git a/app/validators/tags_validator.rb b/app/validators/tags_validator.rb
index b8f3520aa..4b55c5cf9 100644
--- a/app/validators/tags_validator.rb
+++ b/app/validators/tags_validator.rb
@@ -7,18 +7,18 @@ class TagsValidator < ActiveModel::EachValidator
tag if stable_id.nil?
end
- invalid_for_draft_revision = invalid_tags_for_revision(record, attribute, tags, procedure.draft_revision_id)
+ invalid_for_draft_revision = invalid_tags_for_revision(record, attribute, tags, procedure.draft_revision)
invalid_for_published_revision = if procedure.published_revision_id.present?
- invalid_tags_for_revision(record, attribute, tags, procedure.published_revision_id)
+ invalid_tags_for_revision(record, attribute, tags, procedure.published_revision)
else
[]
end
invalid_for_previous_revision = procedure
- .revision_ids_with_pending_dossiers
- .flat_map do |revision_id|
- invalid_tags_for_revision(record, attribute, tags, revision_id)
+ .revisions_with_pending_dossiers
+ .flat_map do |revision|
+ invalid_tags_for_revision(record, attribute, tags, revision)
end.uniq
# champ is added in draft revision but not yet published
@@ -48,12 +48,12 @@ class TagsValidator < ActiveModel::EachValidator
end
end
- def invalid_tags_for_revision(record, attribute, tags, revision_id)
- revision_stable_ids = TypeDeChamp
- .joins(:revision_types_de_champ)
- .where(procedure_revision_types_de_champ: { revision_id: revision_id, parent_id: nil })
- .distinct(:stable_id)
- .pluck(:stable_id)
+ def invalid_tags_for_revision(record, attribute, tags, revision)
+ revision_stable_ids = revision
+ .revision_types_de_champ
+ .filter { !_1.child? }
+ .map(&:stable_id)
+ .uniq
tags.filter_map do |(tag, stable_id)|
if stable_id.present? && !stable_id.in?(revision_stable_ids)
diff --git a/app/views/administrateurs/activate/new.html.haml b/app/views/administrateurs/activate/new.html.haml
index 5d17e6aa8..b826ddffe 100644
--- a/app/views/administrateurs/activate/new.html.haml
+++ b/app/views/administrateurs/activate/new.html.haml
@@ -1,21 +1,26 @@
-- content_for(:title, "Choix du mot de passe")
+- content_for(:title, t('.title'))
- content_for :footer do
= render partial: "root/footer"
-.container.devise-container
- .one-column-centered
- = form_for @administrateur, url: { controller: 'administrateurs/activate', action: :create }, html: { class: "form" } do |f|
- %br
- %h1
- Choix du mot de passe
+.fr-container.fr-my-5w
+ .fr-grid-row.fr-grid-row--center
+ .fr-col-lg-6
+ = form_for @administrateur, url: { controller: 'administrateurs/activate', action: :create } do |f|
+ = f.hidden_field :reset_password_token, value: @token
- = f.hidden_field :reset_password_token, value: @token
- = f.label :email, "Email"
- = f.text_field :email, disabled: true
+ %fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'edit-password-legend' } }
+ %legend.fr-fieldset__legend#edit-password-legend
+ %h1.fr-h2= t('.title')
- = f.label :password do
- Mot de passe
- = render 'password_complexity/field', { form: f, test_complexity: true }
+ .fr-fieldset__element
+ = render Dsfr::InputComponent.new(form: f, attribute: :email, opts: { disabled: true })
- = f.submit 'Continuer', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi..." }
+ .fr-fieldset__element
+ = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
+ opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }})
+
+ #password_complexity
+ = render PasswordComplexityComponent.new
+
+ = f.submit t('.continue'), id: 'submit-password', class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }
diff --git a/app/views/devise/_password_rules.html.haml b/app/views/devise/_password_rules.html.haml
new file mode 100644
index 000000000..2d4083d2f
--- /dev/null
+++ b/app/views/devise/_password_rules.html.haml
@@ -0,0 +1,3 @@
+.fr-messages-group{ "aria-live" => "off", id: id }
+ %p.fr-message= t('views.registrations.new.password_message')
+ %p.fr-message.fr-message--info= t('views.registrations.new.password_placeholder', min_length: PASSWORD_MIN_LENGTH)
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
index 46d33dd55..62103f214 100644
--- a/app/views/devise/passwords/edit.html.haml
+++ b/app/views/devise/passwords/edit.html.haml
@@ -3,20 +3,31 @@
- content_for :footer do
= render partial: 'root/footer'
-.container.devise-container
- .one-column-centered
- = devise_error_messages!
+.fr-container.fr-my-5w
+ .fr-grid-row.fr-grid-row--center
+ .fr-col-lg-6
+ = devise_error_messages!
- = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :patch, class: 'form' }) do |f|
+ = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :patch, class: '' }) do |f|
+ = f.hidden_field :reset_password_token
- %h1 Changement de mot de passe
- = f.hidden_field :reset_password_token
+ %fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'edit-password-legend' } }
+ %legend.fr-fieldset__legend#edit-password-legend
+ %h1.fr-h2= I18n.t('views.users.passwords.edit.subtitle')
- = f.label 'Nouveau mot de passe'
- = render 'password_complexity/field', { form: f, test_complexity: populated_resource.validate_password_complexity? }
+ .fr-fieldset__element
+ = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
+ opts: { autofocus: 'true', autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH, data: { controller: populated_resource.validate_password_complexity? ? 'turbo-input' : false, turbo_input_url_value: show_password_complexity_path }}) do |c|
+ - c.describedby do
+ - if populated_resource.validate_password_complexity?
+ %div{ id: c.describedby_id }
+ #password_complexity
+ = render PasswordComplexityComponent.new
+ - else
+ = render partial: "devise/password_rules", locals: { id: c.describedby_id }
- = f.label 'Confirmez le nouveau mot de passe'
- = f.password_field :password_confirmation, autocomplete: 'off'
+ .fr-fieldset__element
+ = render Dsfr::InputComponent.new(form: f, attribute: :password_confirmation, input_type: :password_field, opts: { autocomplete: 'new-password' })
- = f.submit 'Changer le mot de passe', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi…" }
+ = f.submit t('views.users.passwords.edit.submit'), id: 'submit-password', class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }
diff --git a/app/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed.html.haml b/app/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed.html.haml
new file mode 100644
index 000000000..50ba415ef
--- /dev/null
+++ b/app/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed.html.haml
@@ -0,0 +1,11 @@
+%p
+ Bonjour,
+
+%p
+ = t('administrateurs.groupe_instructeurs.notify_group_when_instructeurs_removed.email_body', count: @removed_instructeur_emails.size, emails: @removed_instructeur_emails.join(', '), groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle)
+
+%p
+ Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe :
+ = link_to(@group.label, admin_procedure_groupe_instructeur_url(@group.procedure, @group))
+
+= render partial: "layouts/mailers/signature"
diff --git a/app/views/groupe_instructeur_mailer/notify_removed_instructeur.html.haml b/app/views/groupe_instructeur_mailer/notify_removed_instructeur.html.haml
new file mode 100644
index 000000000..e093dadcd
--- /dev/null
+++ b/app/views/groupe_instructeur_mailer/notify_removed_instructeur.html.haml
@@ -0,0 +1,8 @@
+%p
+ Bonjour,
+
+%p
+ - assignment_state = @still_assigned_to_procedure ? 'assigned' : 'unassigned'
+ = t("administrateurs.groupe_instructeurs.notify_removed_instructeur.#{assignment_state}.email_body", groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle)
+
+= render partial: "layouts/mailers/signature"
diff --git a/app/views/groupe_instructeur_mailer/remove_instructeur.html.haml b/app/views/groupe_instructeur_mailer/remove_instructeur.html.haml
deleted file mode 100644
index 9e7770919..000000000
--- a/app/views/groupe_instructeur_mailer/remove_instructeur.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-%p
- Bonjour,
-
-%p
- = t('administrateurs.groupe_instructeurs.remove_instructeur.email_body', count: 1, emails: @email, groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle)
-
-%p
- Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe :
- = link_to(@group.label, admin_procedure_groupe_instructeur_url(@group.procedure, @group))
-
-= render partial: "layouts/mailers/signature"
diff --git a/app/views/groupe_instructeur_mailer/remove_instructeurs.html.haml b/app/views/groupe_instructeur_mailer/remove_instructeurs.html.haml
deleted file mode 100644
index d5a73e165..000000000
--- a/app/views/groupe_instructeur_mailer/remove_instructeurs.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-%p
- Bonjour,
-
-%p
- = t('administrateurs.groupe_instructeurs.remove_instructeur.email_body', count: @removed_instructeur_emails.size, emails: @removed_instructeur_emails.join(', '), groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle)
-
-%p
- Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe :
- = link_to(@group.label, admin_procedure_groupe_instructeur_url(@group.procedure, @group))
-
-= render partial: "layouts/mailers/signature"
diff --git a/app/views/password_complexity/_bar.html.haml b/app/views/password_complexity/_bar.html.haml
deleted file mode 100644
index a9b8c8262..000000000
--- a/app/views/password_complexity/_bar.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-#complexity-bar.password-complexity{ class: "complexity-#{@length < @min_length ? @score/2 : @score}" }
diff --git a/app/views/password_complexity/_field.html.haml b/app/views/password_complexity/_field.html.haml
deleted file mode 100644
index 2e031f574..000000000
--- a/app/views/password_complexity/_field.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-= form.password_field :password, autofocus: true, autocomplete: 'off', placeholder: 'Mot de passe', data: { controller: test_complexity ? 'turbo-input' : false, turbo_input_url_value: show_password_complexity_path }
-
-- if test_complexity
- #complexity-bar.password-complexity
-
- .explication
- #complexity-label{ style: 'font-weight: bold' }
- Inscrivez un mot de passe.
- Une courte phrase avec ponctuation peut être un mot de passe très sécurisé.
diff --git a/app/views/password_complexity/_label.html.haml b/app/views/password_complexity/_label.html.haml
deleted file mode 100644
index 2e9bda1d0..000000000
--- a/app/views/password_complexity/_label.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-#complexity-label{ style: 'font-weight: bold' }
- - if @length > 0
- - if @length < @min_length
- Le mot de passe doit faire au moins #{@min_length} caractères.
- - else
- - case @score
- - when 0..1
- Mot de passe très vulnérable.
- - when 2...@min_complexity
- Mot de passe vulnérable.
- - when @min_complexity...4
- Mot de passe acceptable. Vous pouvez valider...
ou améliorer votre mot de passe.
- - else
- Félicitations ! Mot de passe suffisamment fort et sécurisé.
- - else
- Inscrivez un mot de passe.
diff --git a/app/views/password_complexity/show.turbo_stream.haml b/app/views/password_complexity/show.turbo_stream.haml
index 3fd3648f6..461ad5b12 100644
--- a/app/views/password_complexity/show.turbo_stream.haml
+++ b/app/views/password_complexity/show.turbo_stream.haml
@@ -1,5 +1,6 @@
-= turbo_stream.replace 'complexity-label', partial: 'label'
-= turbo_stream.replace 'complexity-bar', partial: 'bar'
+= turbo_stream.update 'password_complexity' do
+ = render PasswordComplexityComponent.new(length: @length, min_length: @min_length, score: @score, min_complexity: @min_complexity)
+
- if @score < @min_complexity || @length < @min_length
= turbo_stream.disable 'submit-password'
- else
diff --git a/app/views/shared/_procedure_description.html.haml b/app/views/shared/_procedure_description.html.haml
index e10d4607f..6500210d7 100644
--- a/app/views/shared/_procedure_description.html.haml
+++ b/app/views/shared/_procedure_description.html.haml
@@ -22,6 +22,6 @@
%p Vous pouvez déposer vos dossiers jusqu’au #{procedure_auto_archive_datetime(procedure)}.
.procedure-description
- .procedure-description-body.read-more-enabled.read-more-collapsed
+ .procedure-description-body.read-more-enabled.read-more-collapsed{ tabindex: "0", role: "region", "aria-label": t('views.users.dossiers.identite.description') }
= h string_to_html(procedure.description, allow_a: true)
= button_tag "Afficher la description complète", class: 'button read-more-button'
diff --git a/app/views/users/registrations/new.html.haml b/app/views/users/registrations/new.html.haml
index feb6e0870..e86419ad2 100644
--- a/app/views/users/registrations/new.html.haml
+++ b/app/views/users/registrations/new.html.haml
@@ -18,10 +18,8 @@
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email', autofocus: true })
.fr-fieldset__element
- = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'new-password', min_length: PASSWORD_MIN_LENGTH }) do |c|
+ = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH }) do |c|
- c.describedby do
- #password-input-messages.fr-messages-group{ "aria-live" => "off" }
- %p#password-input-message.fr-message= t('views.registrations.new.password_message')
- %p#password-input-message-info.fr-message.fr-message--info= t('views.registrations.new.password_placeholder', min_length: PASSWORD_MIN_LENGTH)
+ = render partial: "devise/password_rules", locals: { id: c.describedby_id }
= f.submit t('views.shared.account.create'), class: "fr-btn fr-btn--lg"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index fad29d059..6410dadb8 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -265,6 +265,7 @@ en:
identity_data: Identity data
all_required: All fields are required.
civility: Civility
+ description: Description of the procedure
first_name: First Name
last_name: Last Name
birthdate: Date de naissance
@@ -350,6 +351,10 @@ en:
connect_with_agent_connect: Visit our dedicated page
subtitle: "Sign in with my account"
passwords:
+ edit:
+ subtitle: Change password
+ submit: Change password
+ submit_loading: Sending…
reset_link_sent:
got_it: Got it!
open_your_mailbox: Now open your mailbox.
@@ -471,6 +476,10 @@ en:
attributes:
value:
not_in_options: "must be in the given options"
+ "champs/multiple_drop_down_list_champ":
+ attributes:
+ value:
+ not_in_options: "must be in the given options"
"champs/region_champ":
attributes:
value:
@@ -571,6 +580,11 @@ en:
deleted:
one: Deleted
other: Deleted
+ administrateurs:
+ activate:
+ new:
+ title: Pick a password
+ continue: Continue
users:
dossiers:
test_procedure: "This file is submitted on a test procedure. Any modification of the procedure by the administrator (addition of a field, publication of the procedure, etc.) will result in the removal of the file."
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index b1492cc79..70c52acfc 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -261,6 +261,7 @@ fr:
identity_data: Données d’identité
all_required: Tous les champs sont obligatoires.
civility: Civilité
+ description: Description de la démarche
first_name: Prénom
last_name: Nom
birthdate: Date de naissance
@@ -346,6 +347,10 @@ fr:
connect_with_agent_connect: Accédez à notre page dédiée
subtitle: "Se connecter avec son compte"
passwords:
+ edit:
+ subtitle: Changement de mot de passe
+ submit: Changer le mot de passe
+ submit_loading: Envoi…
reset_link_sent:
email_sent_html: "Nous vous avons envoyé un email à l’adresse %{email}."
click_link_to_reset_password: "Cliquez sur le lien contenu dans l’email pour changer votre mot de passe."
@@ -466,6 +471,10 @@ fr:
attributes:
value:
not_in_options: "doit être dans les options proposées"
+ "champs/multiple_drop_down_list_champ":
+ attributes:
+ value:
+ not_in_options: "doit être dans les options proposées"
"champs/region_champ":
attributes:
value:
@@ -620,6 +629,10 @@ fr:
to_follow: à suivre
total: dossiers
administrateurs:
+ activate:
+ new:
+ title: Choix du mot de passe
+ continue: Continuer
index:
restored: La démarche %{procedure_id} a été restaurée
dropdown_actions:
diff --git a/config/locales/views/administrateurs/groupe_instructeurs/en.yml b/config/locales/views/administrateurs/groupe_instructeurs/en.yml
index 208cd053f..49b54a6db 100644
--- a/config/locales/views/administrateurs/groupe_instructeurs/en.yml
+++ b/config/locales/views/administrateurs/groupe_instructeurs/en.yml
@@ -19,10 +19,15 @@ en:
email_body:
one: "The instructor %{emails} was assigned to the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »."
other: "The instructors %{emails} were assigned to the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »."
- remove_instructeur:
+ notify_group_when_instructeurs_removed:
email_body:
one: "The instructor %{emails} was removed from the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »."
other: "The instructors %{emails} were removed from the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »."
+ notify_removed_instructeur:
+ unassigned:
+ email_body: "You were unassigned from the procedure « %{procedure} » by « %{email} »."
+ assigned:
+ email_body: "You were removed from the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »."
reaffecter_dossiers:
existing_groupe:
one: "%{count} group exist"
diff --git a/config/locales/views/administrateurs/groupe_instructeurs/fr.yml b/config/locales/views/administrateurs/groupe_instructeurs/fr.yml
index dc26ca649..7ebd44c7d 100644
--- a/config/locales/views/administrateurs/groupe_instructeurs/fr.yml
+++ b/config/locales/views/administrateurs/groupe_instructeurs/fr.yml
@@ -25,10 +25,15 @@ fr:
email_body:
one: "L’instructeur %{emails} a été affecté au groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »."
other: "Les instructeurs %{emails} ont été affectés au groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »."
- remove_instructeur:
+ notify_group_when_instructeurs_removed:
email_body:
one: "L’instructeur %{emails} a été retiré du groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »."
other: "Les instructeurs %{emails} ont été retirés du groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »."
+ notify_removed_instructeur:
+ assigned:
+ email_body: "Vous avez été retiré du groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »."
+ unassigned:
+ email_body: "Vous avez été désaffecté de la démarche « %{procedure} » par « %{email} »."
reaffecter_dossiers:
existing_groupe:
one: "%{count} groupe existe"
diff --git a/db/migrate/20230216130722_fix_active_storage_attachment_missing_fk_on_blob_id.rb b/db/migrate/20230216130722_fix_active_storage_attachment_missing_fk_on_blob_id.rb
new file mode 100644
index 000000000..51b697797
--- /dev/null
+++ b/db/migrate/20230216130722_fix_active_storage_attachment_missing_fk_on_blob_id.rb
@@ -0,0 +1,5 @@
+class FixActiveStorageAttachmentMissingFkOnBlobId < ActiveRecord::Migration[6.1]
+ def change
+ add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id, validate: false
+ end
+end
diff --git a/lib/tasks/deployment/20230216135218_reclean_attachments.rake b/lib/tasks/deployment/20230216135218_reclean_attachments.rake
new file mode 100644
index 000000000..cfc5eb52f
--- /dev/null
+++ b/lib/tasks/deployment/20230216135218_reclean_attachments.rake
@@ -0,0 +1,21 @@
+namespace :after_party do
+ desc 'Deployment task: reclean_attachments'
+ task reclean_attachments: :environment do
+ puts "Running deploy task 'reclean_attachments'"
+
+ invalid_attachments = ActiveStorage::Attachment.where.missing(:blob)
+ invalid_attachments_count = invalid_attachments.size
+
+ if invalid_attachments.any?
+ invalid_attachments.destroy_all
+ puts "#{invalid_attachments_count} with blob that doesn't exist have been destroyed"
+ else
+ puts "No attachments with blob that doesn't exist found"
+ end
+
+ # 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
diff --git a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb
index 90e3a193f..384a24756 100644
--- a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb
+++ b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb
@@ -320,11 +320,22 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do
end
context 'when there are many instructeurs' do
- before { remove_instructeur(admin.instructeur) }
+ before do
+ allow(GroupeInstructeurMailer).to receive(:notify_removed_instructeur)
+ .and_return(double(deliver_later: true))
+ remove_instructeur(admin.instructeur)
+ end
it { expect(gi_1_1.instructeurs).to include(instructeur) }
it { expect(gi_1_1.reload.instructeurs.count).to eq(1) }
it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_1)) }
+ it "calls GroupeInstructeurMailer with the right groupe and instructeur" do
+ expect(GroupeInstructeurMailer).to have_received(:notify_removed_instructeur).with(
+ gi_1_1,
+ admin.instructeur,
+ admin.email
+ )
+ end
end
context 'when there is only one instructeur' do
diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb
index 990b3a29c..b6e1af036 100644
--- a/spec/controllers/instructeurs/dossiers_controller_spec.rb
+++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb
@@ -792,7 +792,7 @@ describe Instructeurs::DossiersController, type: :controller do
champs_private_attributes: {
'0': {
id: champ_multiple_drop_down_list.id,
- value: ['', 'un', 'deux']
+ value: ['', 'val1', 'val2']
},
'1': {
id: champ_datetime.id,
@@ -813,7 +813,7 @@ describe Instructeurs::DossiersController, type: :controller do
end
it {
- expect(champ_multiple_drop_down_list.value).to eq('["un", "deux"]')
+ expect(champ_multiple_drop_down_list.value).to eq('["val1", "val2"]')
expect(champ_linked_drop_down_list.primary_value).to eq('primary')
expect(champ_linked_drop_down_list.secondary_value).to eq('secondary')
expect(champ_datetime.value).to eq('2019-12-21T13:17:00+01:00')
@@ -839,7 +839,7 @@ describe Instructeurs::DossiersController, type: :controller do
champs_public_attributes: {
'0': {
id: champ_multiple_drop_down_list.id,
- value: ['', 'un', 'deux']
+ value: ['', 'val1', 'val2']
}
}
}
diff --git a/spec/controllers/password_complexity_controller_spec.rb b/spec/controllers/password_complexity_controller_spec.rb
index b04985806..6d03cab40 100644
--- a/spec/controllers/password_complexity_controller_spec.rb
+++ b/spec/controllers/password_complexity_controller_spec.rb
@@ -27,8 +27,7 @@ describe PasswordComplexityController, type: :controller do
it 'renders Javascript that updates the password complexity meter' do
subject
- expect(response.body).to include('complexity-label')
- expect(response.body).to include('complexity-bar')
+ expect(response.body).to include('Mot de passe vulnérable')
end
end
end
diff --git a/spec/factories/champ.rb b/spec/factories/champ.rb
index 253247a8e..d1f4448f8 100644
--- a/spec/factories/champ.rb
+++ b/spec/factories/champ.rb
@@ -97,7 +97,7 @@ FactoryBot.define do
factory :champ_multiple_drop_down_list, class: 'Champs::MultipleDropDownListChamp' do
type_de_champ { association :type_de_champ_multiple_drop_down_list, procedure: dossier.procedure }
- value { '["choix 1", "choix 2"]' }
+ value { '["val1", "val2"]' }
end
factory :champ_linked_drop_down_list, class: 'Champs::LinkedDropDownListChamp' do
diff --git a/spec/mailers/groupe_instructeur_mailer_spec.rb b/spec/mailers/groupe_instructeur_mailer_spec.rb
index 9157f03ef..6cf1ed75e 100644
--- a/spec/mailers/groupe_instructeur_mailer_spec.rb
+++ b/spec/mailers/groupe_instructeur_mailer_spec.rb
@@ -1,5 +1,5 @@
RSpec.describe GroupeInstructeurMailer, type: :mailer do
- describe '#remove_instructeurs' do
+ describe '#notify_group_when_instructeurs_removed' do
let(:groupe_instructeur) do
gi = GroupeInstructeur.create(label: 'gi1', procedure: create(:procedure))
gi.instructeurs << create(:instructeur, email: 'int1@g')
@@ -7,15 +7,53 @@ RSpec.describe GroupeInstructeurMailer, type: :mailer do
gi.instructeurs << instructeurs_to_remove
gi
end
- let(:instructeur_1) { create(:instructeur, email: 'int3@g') }
- let(:instructeur_2) { create(:instructeur, email: 'int4@g') }
+ let(:instructeur_3) { create(:instructeur, email: 'int3@g') }
+ let(:instructeur_4) { create(:instructeur, email: 'int4@g') }
- let(:instructeurs_to_remove) { [instructeur_1, instructeur_2] }
+ let(:instructeurs_to_remove) { [instructeur_3, instructeur_4] }
let(:current_instructeur_email) { 'toto@email.com' }
- subject { described_class.remove_instructeurs(groupe_instructeur, instructeurs_to_remove, current_instructeur_email) }
+ subject { described_class.notify_group_when_instructeurs_removed(groupe_instructeur, instructeurs_to_remove, current_instructeur_email) }
+
+ before { instructeurs_to_remove.each { groupe_instructeur.remove(_1) } }
it { expect(subject.body).to include('Les instructeurs int3@g, int4@g ont été retirés du groupe') }
- it { expect(subject.bcc).to match_array(['int1@g', 'int2@g', 'int3@g', 'int4@g']) }
+ it { expect(subject.bcc).to match_array(['int1@g', 'int2@g']) }
+ end
+
+ describe '#notify_removed_instructeur' do
+ let(:procedure) { create(:procedure) }
+ let(:groupe_instructeur) do
+ gi = GroupeInstructeur.create(label: 'gi1', procedure: procedure)
+ gi.instructeurs << create(:instructeur, email: 'int1@g')
+ gi.instructeurs << create(:instructeur, email: 'int2@g')
+ gi.instructeurs << instructeur_to_remove
+ gi
+ end
+ let(:instructeur_to_remove) { create(:instructeur, email: 'int3@g') }
+
+ let(:current_instructeur_email) { 'toto@email.com' }
+
+ subject { described_class.notify_removed_instructeur(groupe_instructeur, instructeur_to_remove, current_instructeur_email) }
+
+ before { groupe_instructeur.remove(instructeur_to_remove) }
+
+ context 'when instructeur is fully removed form procedure' do
+ it { expect(subject.body).to include('Vous avez été désaffecté de la démarche') }
+ it { expect(subject.to).to include('int3@g') }
+ it { expect(subject.to).not_to include('int1@g', 'int2@g') }
+ end
+
+ context 'when instructeur is removed from one group but still affected to procedure' do
+ let!(:groupe_instructeur_2) do
+ gi2 = GroupeInstructeur.create(label: 'gi2', procedure: procedure)
+ gi2.instructeurs << instructeur_to_remove
+ gi2
+ end
+
+ it { expect(subject.body).to include('Vous avez été retiré du groupe « gi1 » par « toto@email.com »') }
+ it { expect(subject.to).to include('int3@g') }
+ it { expect(subject.to).not_to include('int1@g', 'int2@g') }
+ end
end
end
diff --git a/spec/mailers/previews/groupe_instructeur_mailer_preview.rb b/spec/mailers/previews/groupe_instructeur_mailer_preview.rb
index 1b6c006a1..4eab9b7d7 100644
--- a/spec/mailers/previews/groupe_instructeur_mailer_preview.rb
+++ b/spec/mailers/previews/groupe_instructeur_mailer_preview.rb
@@ -1,9 +1,17 @@
class GroupeInstructeurMailerPreview < ActionMailer::Preview
- def remove_instructeurs
+ def notify_group_when_instructeurs_removed
procedure = Procedure.new(id: 1, libelle: 'une superbe procedure')
groupe = GroupeInstructeur.new(id: 1, label: 'Val-De-Marne', procedure:)
current_instructeur_email = 'admin@dgfip.com'
instructeurs = Instructeur.limit(2)
- GroupeInstructeurMailer.remove_instructeurs(groupe, instructeurs, current_instructeur_email)
+ GroupeInstructeurMailer.notify_group_when_instructeurs_removed(groupe, instructeurs, current_instructeur_email)
+ end
+
+ def notify_removed_instructeur
+ procedure = Procedure.new(id: 1, libelle: 'une superbe procedure')
+ groupe = GroupeInstructeur.new(id: 1, label: 'Val-De-Marne', procedure:)
+ current_instructeur_email = 'admin@dgfip.com'
+ instructeur = Instructeur.last
+ GroupeInstructeurMailer.notify_removed_instructeur(groupe, instructeur, current_instructeur_email)
end
end
diff --git a/spec/models/champ_spec.rb b/spec/models/champ_spec.rb
index bec9c3097..7cad128a7 100644
--- a/spec/models/champ_spec.rb
+++ b/spec/models/champ_spec.rb
@@ -117,7 +117,7 @@ describe Champ do
# when using the old form, and the ChampsService Class
# TODO: to remove
context 'when the value is already deserialized' do
- let(:value) { '["1", "2"]' }
+ let(:value) { '["val1", "val2"]' }
it { expect(champ.value).to eq(value) }
@@ -133,9 +133,9 @@ describe Champ do
# GOTCHA
context 'when the value is not already deserialized' do
context 'when a choice is selected' do
- let(:value) { '["", "1", "2"]' }
+ let(:value) { '["", "val1", "val2"]' }
- it { expect(champ.value).to eq('["1", "2"]') }
+ it { expect(champ.value).to eq('["val1", "val2"]') }
end
context 'when all choices are removed' do
diff --git a/spec/models/champs/multiple_drop_down_list_champ_spec.rb b/spec/models/champs/multiple_drop_down_list_champ_spec.rb
new file mode 100644
index 000000000..08a78c77c
--- /dev/null
+++ b/spec/models/champs/multiple_drop_down_list_champ_spec.rb
@@ -0,0 +1,38 @@
+describe Champs::MultipleDropDownListChamp do
+ describe 'validations' do
+ describe 'inclusion' do
+ let(:type_de_champ) { build(:type_de_champ_multiple_drop_down_list, drop_down_list_value: "val1\r\nval2\r\nval3") }
+ subject { build(:champ_multiple_drop_down_list, type_de_champ:, value:) }
+
+ context 'when the value is nil' do
+ let(:value) { nil }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when the value is an empty string' do
+ let(:value) { '' }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when the value is an empty array' do
+ let(:value) { [] }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when the value is included in the option list' do
+ let(:value) { ["val3", "val1"] }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when the value is not included in the option list' do
+ let(:value) { ["totoro", "val1"] }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+ end
+end
diff --git a/spec/models/prefill_params_spec.rb b/spec/models/prefill_params_spec.rb
index b7e03b27b..bf9378ae0 100644
--- a/spec/models/prefill_params_spec.rb
+++ b/spec/models/prefill_params_spec.rb
@@ -128,6 +128,7 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ public value that is authorized", :iban, "value"
it_behaves_like "a champ public value that is authorized", :civilite, "M."
it_behaves_like "a champ public value that is authorized", :pays, "FR"
+ it_behaves_like "a champ public value that is authorized", :regions, "03"
it_behaves_like "a champ public value that is authorized", :date, "2022-12-22"
it_behaves_like "a champ public value that is authorized", :datetime, "2022-12-22T10:30"
it_behaves_like "a champ public value that is authorized", :yes_no, "true"
@@ -135,8 +136,8 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ public value that is authorized", :checkbox, "true"
it_behaves_like "a champ public value that is authorized", :checkbox, "false"
it_behaves_like "a champ public value that is authorized", :drop_down_list, "value"
- it_behaves_like "a champ public value that is authorized", :regions, "03"
it_behaves_like "a champ public value that is authorized", :departements, "03"
+ it_behaves_like "a champ public value that is authorized", :multiple_drop_down_list, ["val1", "val2"]
it_behaves_like "a champ public value that is authorized", :epci, ['01', '200042935']
it_behaves_like "a champ private value that is authorized", :text, "value"
@@ -148,6 +149,7 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ private value that is authorized", :iban, "value"
it_behaves_like "a champ private value that is authorized", :civilite, "M."
it_behaves_like "a champ private value that is authorized", :pays, "FR"
+ it_behaves_like "a champ private value that is authorized", :regions, "93"
it_behaves_like "a champ private value that is authorized", :date, "2022-12-22"
it_behaves_like "a champ private value that is authorized", :datetime, "2022-12-22T10:30"
it_behaves_like "a champ private value that is authorized", :yes_no, "true"
@@ -157,6 +159,7 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ private value that is authorized", :drop_down_list, "value"
it_behaves_like "a champ private value that is authorized", :regions, "93"
it_behaves_like "a champ private value that is authorized", :departements, "03"
+ it_behaves_like "a champ private value that is authorized", :multiple_drop_down_list, ["val1", "val2"]
it_behaves_like "a champ private value that is authorized", :epci, ['01', '200042935']
it_behaves_like "a champ public value that is unauthorized", :decimal_number, "non decimal string"
@@ -169,7 +172,6 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ public value that is unauthorized", :date, "value"
it_behaves_like "a champ public value that is unauthorized", :datetime, "value"
it_behaves_like "a champ public value that is unauthorized", :datetime, "12-22-2022T10:30"
- it_behaves_like "a champ public value that is unauthorized", :multiple_drop_down_list, "value"
it_behaves_like "a champ public value that is unauthorized", :linked_drop_down_list, "value"
it_behaves_like "a champ public value that is unauthorized", :header_section, "value"
it_behaves_like "a champ public value that is unauthorized", :explication, "value"
@@ -187,6 +189,7 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ public value that is unauthorized", :siret, "value"
it_behaves_like "a champ public value that is unauthorized", :rna, "value"
it_behaves_like "a champ public value that is unauthorized", :annuaire_education, "value"
+ it_behaves_like "a champ public value that is unauthorized", :multiple_drop_down_list, ["value"]
end
private
diff --git a/spec/models/type_de_champ_spec.rb b/spec/models/type_de_champ_spec.rb
index cbd34f501..d7f5c5502 100644
--- a/spec/models/type_de_champ_spec.rb
+++ b/spec/models/type_de_champ_spec.rb
@@ -95,7 +95,7 @@ describe TypeDeChamp do
let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) }
it 'removes the children types de champ' do
- expect(procedure.draft_revision.children_of(tdc)).to be_empty
+ expect(procedure.draft_revision.reload.children_of(tdc)).to be_empty
end
end
end
@@ -246,18 +246,18 @@ describe TypeDeChamp do
it_behaves_like "a prefillable type de champ", :type_de_champ_datetime
it_behaves_like "a prefillable type de champ", :type_de_champ_civilite
it_behaves_like "a prefillable type de champ", :type_de_champ_pays
+ it_behaves_like "a prefillable type de champ", :type_de_champ_regions
it_behaves_like "a prefillable type de champ", :type_de_champ_yes_no
it_behaves_like "a prefillable type de champ", :type_de_champ_checkbox
it_behaves_like "a prefillable type de champ", :type_de_champ_drop_down_list
- it_behaves_like "a prefillable type de champ", :type_de_champ_regions
it_behaves_like "a prefillable type de champ", :type_de_champ_departements
+ it_behaves_like "a prefillable type de champ", :type_de_champ_multiple_drop_down_list
it_behaves_like "a prefillable type de champ", :type_de_champ_epci
it_behaves_like "a non-prefillable type de champ", :type_de_champ_number
it_behaves_like "a non-prefillable type de champ", :type_de_champ_communes
it_behaves_like "a non-prefillable type de champ", :type_de_champ_dossier_link
it_behaves_like "a non-prefillable type de champ", :type_de_champ_titre_identite
- it_behaves_like "a non-prefillable type de champ", :type_de_champ_multiple_drop_down_list
it_behaves_like "a non-prefillable type de champ", :type_de_champ_linked_drop_down_list
it_behaves_like "a non-prefillable type de champ", :type_de_champ_header_section
it_behaves_like "a non-prefillable type de champ", :type_de_champ_explication
diff --git a/spec/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ_spec.rb
new file mode 100644
index 000000000..d44fd1274
--- /dev/null
+++ b/spec/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.describe TypesDeChamp::PrefillMultipleDropDownListTypeDeChamp do
+ describe 'ancestors' do
+ subject { described_class.new(build(:type_de_champ_multiple_drop_down_list)) }
+
+ it { is_expected.to be_kind_of(TypesDeChamp::PrefillDropDownListTypeDeChamp) }
+ end
+
+ describe '#example_value' do
+ let(:type_de_champ) { build(:type_de_champ_multiple_drop_down_list, drop_down_list_value: drop_down_list_value) }
+ subject(:example_value) { described_class.new(type_de_champ).example_value }
+
+ context 'when the multiple drop down list has no option' do
+ let(:drop_down_list_value) { "" }
+
+ it { expect(example_value).to eq(nil) }
+ end
+
+ context 'when the multiple drop down list only has one option' do
+ let(:drop_down_list_value) { "value" }
+
+ it { expect(example_value).to eq("value") }
+ end
+
+ context 'when the multiple drop down list has two options or more' do
+ let(:drop_down_list_value) { "value1\r\nvalue2\r\nvalue3" }
+
+ it { expect(example_value).to eq(["value1", "value2"]) }
+ end
+ end
+end
diff --git a/spec/models/types_de_champ/prefill_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_type_de_champ_spec.rb
index 7477b3789..3f892f907 100644
--- a/spec/models/types_de_champ/prefill_type_de_champ_spec.rb
+++ b/spec/models/types_de_champ/prefill_type_de_champ_spec.rb
@@ -10,6 +10,12 @@ RSpec.describe TypesDeChamp::PrefillTypeDeChamp, type: :model do
it { expect(built).to be_kind_of(TypesDeChamp::PrefillDropDownListTypeDeChamp) }
end
+ context 'when the type de champ is a multiple_drop_down_list' do
+ let(:type_de_champ) { build(:type_de_champ_multiple_drop_down_list) }
+
+ it { expect(built).to be_kind_of(TypesDeChamp::PrefillMultipleDropDownListTypeDeChamp) }
+ end
+
context 'when the type de champ is a pays' do
let(:type_de_champ) { build(:type_de_champ_pays) }
diff --git a/spec/support/shared_examples_for_prefilled_dossier.rb b/spec/support/shared_examples_for_prefilled_dossier.rb
index 5ed67c49c..5cd01bd2e 100644
--- a/spec/support/shared_examples_for_prefilled_dossier.rb
+++ b/spec/support/shared_examples_for_prefilled_dossier.rb
@@ -19,6 +19,9 @@ shared_examples "the user has got a prefilled dossier, owned by themselves" do
expect(page).to have_field(type_de_champ_phone.libelle, with: phone_value)
expect(page).to have_css('label', text: type_de_champ_phone.libelle)
expect(page).to have_field(type_de_champ_datetime.libelle, with: datetime_value)
+ expect(page).to have_css('label', text: type_de_champ_multiple_drop_down_list.libelle)
+ expect(page).to have_content(multiple_drop_down_list_values.first)
+ expect(page).to have_content(multiple_drop_down_list_values.last)
expect(page).to have_field(type_de_champ_epci.libelle, with: epci_value.last)
end
end
diff --git a/spec/system/users/dossier_prefill_get_spec.rb b/spec/system/users/dossier_prefill_get_spec.rb
index 41fc21403..2e0523bfd 100644
--- a/spec/system/users/dossier_prefill_get_spec.rb
+++ b/spec/system/users/dossier_prefill_get_spec.rb
@@ -9,12 +9,30 @@ describe 'Prefilling a dossier (with a GET request):' do
let(:type_de_champ_text) { create(:type_de_champ_text, procedure: procedure) }
let(:type_de_champ_phone) { create(:type_de_champ_phone, procedure: procedure) }
let(:type_de_champ_datetime) { create(:type_de_champ_datetime, procedure: procedure) }
+ let(:type_de_champ_multiple_drop_down_list) { create(:type_de_champ_multiple_drop_down_list, procedure: procedure) }
let(:type_de_champ_epci) { create(:type_de_champ_epci, procedure: procedure) }
let(:text_value) { "My Neighbor Totoro is the best movie ever" }
let(:phone_value) { "invalid phone value" }
let(:datetime_value) { "2023-02-01T10:32" }
+ let(:multiple_drop_down_list_values) {
+ [
+ type_de_champ_multiple_drop_down_list.drop_down_list_enabled_non_empty_options.first,
+ type_de_champ_multiple_drop_down_list.drop_down_list_enabled_non_empty_options.last
+ ]
+ }
let(:epci_value) { ['01', '200029999'] }
+ let(:entry_path) {
+ commencer_path(
+ path: procedure.path,
+ "champ_#{type_de_champ_text.to_typed_id}" => text_value,
+ "champ_#{type_de_champ_phone.to_typed_id}" => phone_value,
+ "champ_#{type_de_champ_datetime.to_typed_id}" => datetime_value,
+ "champ_#{type_de_champ_multiple_drop_down_list.to_typed_id}" => multiple_drop_down_list_values,
+ "champ_#{type_de_champ_epci.to_typed_id}" => epci_value
+ )
+ }
+
before do
allow(Rails).to receive(:cache).and_return(memory_store)
Rails.cache.clear
@@ -36,13 +54,7 @@ describe 'Prefilling a dossier (with a GET request):' do
visit "/users/sign_in"
sign_in_with user.email, password
- visit commencer_path(
- path: procedure.path,
- "champ_#{type_de_champ_text.to_typed_id}" => text_value,
- "champ_#{type_de_champ_phone.to_typed_id}" => phone_value,
- "champ_#{type_de_champ_datetime.to_typed_id}" => datetime_value,
- "champ_#{type_de_champ_epci.to_typed_id}" => epci_value
- )
+ visit entry_path
click_on "Poursuivre mon dossier prérempli"
end
@@ -50,15 +62,7 @@ describe 'Prefilling a dossier (with a GET request):' do
end
context 'when unauthenticated' do
- before do
- visit commencer_path(
- path: procedure.path,
- "champ_#{type_de_champ_text.to_typed_id}" => text_value,
- "champ_#{type_de_champ_phone.to_typed_id}" => phone_value,
- "champ_#{type_de_champ_datetime.to_typed_id}" => datetime_value,
- "champ_#{type_de_champ_epci.to_typed_id}" => epci_value
- )
- end
+ before { visit entry_path }
context 'when the user signs in with email and password' do
it_behaves_like "the user has got a prefilled dossier, owned by themselves" do
diff --git a/spec/system/users/dossier_prefill_post_spec.rb b/spec/system/users/dossier_prefill_post_spec.rb
index c0eaa03fe..454919e67 100644
--- a/spec/system/users/dossier_prefill_post_spec.rb
+++ b/spec/system/users/dossier_prefill_post_spec.rb
@@ -9,10 +9,17 @@ describe 'Prefilling a dossier (with a POST request):' do
let(:type_de_champ_text) { create(:type_de_champ_text, procedure: procedure) }
let(:type_de_champ_phone) { create(:type_de_champ_phone, procedure: procedure) }
let(:type_de_champ_datetime) { create(:type_de_champ_datetime, procedure: procedure) }
+ let(:type_de_champ_multiple_drop_down_list) { create(:type_de_champ_multiple_drop_down_list, procedure: procedure) }
let(:type_de_champ_epci) { create(:type_de_champ_epci, procedure: procedure) }
let(:text_value) { "My Neighbor Totoro is the best movie ever" }
let(:phone_value) { "invalid phone value" }
let(:datetime_value) { "2023-02-01T10:32" }
+ let(:multiple_drop_down_list_values) {
+ [
+ type_de_champ_multiple_drop_down_list.drop_down_list_enabled_non_empty_options.first,
+ type_de_champ_multiple_drop_down_list.drop_down_list_enabled_non_empty_options.last
+ ]
+ }
let(:epci_value) { ['01', '200029999'] }
before do
@@ -116,6 +123,7 @@ describe 'Prefilling a dossier (with a POST request):' do
"champ_#{type_de_champ_text.to_typed_id}" => text_value,
"champ_#{type_de_champ_phone.to_typed_id}" => phone_value,
"champ_#{type_de_champ_datetime.to_typed_id}" => datetime_value,
+ "champ_#{type_de_champ_multiple_drop_down_list.to_typed_id}" => multiple_drop_down_list_values,
"champ_#{type_de_champ_epci.to_typed_id}" => epci_value
}.to_json
JSON.parse(session.response.body)["dossier_url"].gsub("http://www.example.com", "")