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", "")