Merge pull request #8670 from mfo/US/markdown-like
amelioration(a11y): rend du texte saisi (par les admin/instructeur/expert) en html compatible avec les contraintes d'accessibilités
This commit is contained in:
commit
d0e05784e9
32 changed files with 206 additions and 133 deletions
1
Gemfile
1
Gemfile
|
@ -70,6 +70,7 @@ gem 'rack-attack'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
gem 'rails-i18n' # Locales par défaut
|
gem 'rails-i18n' # Locales par défaut
|
||||||
gem 'rake-progressbar', require: false
|
gem 'rake-progressbar', require: false
|
||||||
|
gem 'redcarpet'
|
||||||
gem 'rexml' # add missing gem due to ruby3 (https://github.com/Shopify/bootsnap/issues/325)
|
gem 'rexml' # add missing gem due to ruby3 (https://github.com/Shopify/bootsnap/issues/325)
|
||||||
gem 'rgeo-geojson'
|
gem 'rgeo-geojson'
|
||||||
gem 'rqrcode'
|
gem 'rqrcode'
|
||||||
|
|
|
@ -573,6 +573,7 @@ GEM
|
||||||
rb-fsevent (0.10.4)
|
rb-fsevent (0.10.4)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
|
redcarpet (3.6.0)
|
||||||
regexp_parser (2.6.0)
|
regexp_parser (2.6.0)
|
||||||
request_store (1.5.0)
|
request_store (1.5.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
|
@ -909,6 +910,7 @@ DEPENDENCIES
|
||||||
rails-erd
|
rails-erd
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rake-progressbar
|
rake-progressbar
|
||||||
|
redcarpet
|
||||||
rexml
|
rexml
|
||||||
rgeo-geojson
|
rgeo-geojson
|
||||||
rqrcode
|
rqrcode
|
||||||
|
|
|
@ -5,8 +5,6 @@ $procedure-context-breakpoint: $two-columns-breakpoint;
|
||||||
$procedure-description-line-height: 22px;
|
$procedure-description-line-height: 22px;
|
||||||
|
|
||||||
.procedure-preview {
|
.procedure-preview {
|
||||||
font-size: 24px;
|
|
||||||
|
|
||||||
.paperless-logo {
|
.paperless-logo {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 60px;
|
margin-bottom: 60px;
|
||||||
|
@ -74,17 +72,6 @@ $procedure-description-line-height: 22px;
|
||||||
border-bottom: 1px dotted $blue-france-500;
|
border-bottom: 1px dotted $blue-france-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.procedure-description {
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
p:not(:last-of-type) {
|
|
||||||
// Space the paragraphs by exactly one line height,
|
|
||||||
// so that the text always is truncated in the middle of a line,
|
|
||||||
// regarless of the number of paragraphs.
|
|
||||||
margin-bottom: 3 * $default-spacer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-more-button {
|
.read-more-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,15 +55,6 @@ class Dossiers::MessageComponent < ApplicationComponent
|
||||||
l(commentaire.created_at, format: is_current_year ? :message_date : :message_date_with_year)
|
l(commentaire.created_at, format: is_current_year ? :message_date : :message_date_with_year)
|
||||||
end
|
end
|
||||||
|
|
||||||
def commentaire_body
|
|
||||||
if commentaire.discarded?
|
|
||||||
t('.deleted_body')
|
|
||||||
else
|
|
||||||
body_formatted = commentaire.sent_by_system? ? commentaire.body : simple_format(commentaire.body)
|
|
||||||
sanitize(body_formatted, commentaire.sent_by_system? ? { scrubber: Sanitizers::MailScrubber.new } : {})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def highlight?
|
def highlight?
|
||||||
commentaire.created_at.present? && @messagerie_seen_at&.<(commentaire.created_at)
|
commentaire.created_at.present? && @messagerie_seen_at&.<(commentaire.created_at)
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,14 @@
|
||||||
%span.fr-text--xs.fr-text-mention--grey.font-weight-normal= t('.guest')
|
%span.fr-text--xs.fr-text-mention--grey.font-weight-normal= t('.guest')
|
||||||
%span.date{ class: ["fr-text--xs", "fr-text-mention--grey", "font-weight-normal", highlight_if_unseen_class], data: scroll_to_target }
|
%span.date{ class: ["fr-text--xs", "fr-text-mention--grey", "font-weight-normal", highlight_if_unseen_class], data: scroll_to_target }
|
||||||
= commentaire_date
|
= commentaire_date
|
||||||
.rich-text= commentaire_body
|
.rich-text
|
||||||
|
- if commentaire.discarded?
|
||||||
|
%p= t('.deleted_body')
|
||||||
|
- elsif commentaire.sent_by_system?
|
||||||
|
= sanitize(commentaire.body, scrubber: Sanitizers::MailScrubber.new)
|
||||||
|
- else
|
||||||
|
= render SimpleFormatComponent.new(commentaire.body, allow_a: false)
|
||||||
|
|
||||||
|
|
||||||
.message-extras.flex.justify-start
|
.message-extras.flex.justify-start
|
||||||
- if commentaire.soft_deletable?(connected_user)
|
- if commentaire.soft_deletable?(connected_user)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# see: https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/mise-en-avant
|
# see: https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/mise-en-avant
|
||||||
class Dsfr::CalloutComponent < ApplicationComponent
|
class Dsfr::CalloutComponent < ApplicationComponent
|
||||||
renders_one :body
|
renders_one :body
|
||||||
|
renders_one :html_body
|
||||||
renders_one :bottom
|
renders_one :bottom
|
||||||
|
|
||||||
attr_reader :title, :theme, :icon, :extra_class_names
|
attr_reader :title, :theme, :icon, :extra_class_names
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
%div{ class: callout_class }
|
%div{ class: callout_class }
|
||||||
- if title.present?
|
- if title.present?
|
||||||
%h3.fr-callout__title= title
|
%h3.fr-callout__title= title
|
||||||
%p.fr-callout__text= body
|
- if html_body?
|
||||||
|
.fr-callout__text= html_body
|
||||||
|
- if body?
|
||||||
|
%p.fr-callout__text= body
|
||||||
= bottom
|
= bottom
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
class EditableChamp::ChampLabelComponent < ApplicationComponent
|
class EditableChamp::ChampLabelComponent < ApplicationComponent
|
||||||
include StringToHtmlHelper
|
|
||||||
|
|
||||||
def initialize(form:, champ:, seen_at: nil)
|
def initialize(form:, champ:, seen_at: nil)
|
||||||
@form, @champ, @seen_at = form, champ, seen_at
|
@form, @champ, @seen_at = form, champ, seen_at
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,4 +7,4 @@
|
||||||
= render EditableChamp::ChampLabelContentComponent.new champ: @champ, seen_at: @seen_at
|
= render EditableChamp::ChampLabelContentComponent.new champ: @champ, seen_at: @seen_at
|
||||||
|
|
||||||
- if @champ.description.present?
|
- if @champ.description.present?
|
||||||
.notice{ id: @champ.describedby_id }= string_to_html(@champ.description, allow_a: true)
|
.notice{ id: @champ.describedby_id }= render SimpleFormatComponent.new(@champ.description, allow_a: true)
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
class EditableChamp::EditableChampComponent < ApplicationComponent
|
class EditableChamp::EditableChampComponent < ApplicationComponent
|
||||||
include StringToHtmlHelper
|
|
||||||
|
|
||||||
def initialize(form:, champ:, seen_at: nil)
|
def initialize(form:, champ:, seen_at: nil)
|
||||||
@form, @champ, @seen_at = form, champ, seen_at
|
@form, @champ, @seen_at = form, champ, seen_at
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
- if @champ.block?
|
- if @champ.block?
|
||||||
%h3.header-subsection= @champ.libelle
|
%h3.header-subsection= @champ.libelle
|
||||||
- if @champ.description.present?
|
- if @champ.description.present?
|
||||||
%p.notice= string_to_html(@champ.description, false, allow_a: true)
|
.notice= render SimpleFormatComponent.new(@champ.description, allow_a: true)
|
||||||
|
|
||||||
- elsif has_label?(@champ)
|
- elsif has_label?(@champ)
|
||||||
= render EditableChamp::ChampLabelComponent.new form: @form, champ: @champ, seen_at: @seen_at
|
= render EditableChamp::ChampLabelComponent.new form: @form, champ: @champ, seen_at: @seen_at
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
class EditableChamp::ExplicationComponent < EditableChamp::EditableChampBaseComponent
|
class EditableChamp::ExplicationComponent < EditableChamp::EditableChampBaseComponent
|
||||||
include StringToHtmlHelper
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
= render Dsfr::CalloutComponent.new(title: @champ.libelle, extra_class_names: ['fr-mb-2w', 'fr-callout--blue-cumulus']) do |c|
|
= render Dsfr::CalloutComponent.new(title: @champ.libelle, extra_class_names: ['fr-mb-2w', 'fr-callout--blue-cumulus']) do |c|
|
||||||
- c.with_body do
|
- c.with_html_body do
|
||||||
|
|
||||||
= string_to_html(@champ.description, allow_a: true)
|
= render SimpleFormatComponent.new(@champ.description, allow_a: true)
|
||||||
|
|
||||||
- if @champ.collapsible_explanation_enabled? && @champ.collapsible_explanation_text.present?
|
- if @champ.collapsible_explanation_enabled? && @champ.collapsible_explanation_text.present?
|
||||||
%div
|
%div
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
class EditableChamp::LinkedDropDownListComponent < EditableChamp::EditableChampBaseComponent
|
class EditableChamp::LinkedDropDownListComponent < EditableChamp::EditableChampBaseComponent
|
||||||
include StringToHtmlHelper
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
= @form.label :secondary_value, for: "#{@champ.input_id}-secondary" do
|
= @form.label :secondary_value, for: "#{@champ.input_id}-secondary" do
|
||||||
- sanitize((@champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première") + (@champ.type_de_champ.mandatory? ? tag.span(' *', class: 'mandatory') : ''))
|
- sanitize((@champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première") + (@champ.type_de_champ.mandatory? ? tag.span(' *', class: 'mandatory') : ''))
|
||||||
- if @champ.drop_down_secondary_description.present?
|
- if @champ.drop_down_secondary_description.present?
|
||||||
.notice{ id: "#{@champ.describedby_id}-secondary" }= string_to_html(@champ.drop_down_secondary_description, allow_a: true)
|
.notice{ id: "#{@champ.describedby_id}-secondary" }= render SimpleFormatComponent.new(@champ.drop_down_secondary_description, allow_a: true)
|
||||||
= @form.select :secondary_value,
|
= @form.select :secondary_value,
|
||||||
@champ.secondary_options[@champ.primary_value],
|
@champ.secondary_options[@champ.primary_value],
|
||||||
{},
|
{},
|
||||||
|
|
51
app/components/simple_format_component.rb
Normal file
51
app/components/simple_format_component.rb
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
class SimpleFormatComponent < ApplicationComponent
|
||||||
|
# see: https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
|
||||||
|
REDCARPET_EXTENSIONS = {
|
||||||
|
no_intra_emphasis: false,
|
||||||
|
tables: false,
|
||||||
|
fenced_code_blocks: false,
|
||||||
|
autolink: false,
|
||||||
|
disable_indented_code_blocks: false,
|
||||||
|
strikethrough: false,
|
||||||
|
lax_spacing: false,
|
||||||
|
space_after_headers: false,
|
||||||
|
superscript: false,
|
||||||
|
underline: false,
|
||||||
|
highlight: false,
|
||||||
|
quote: false,
|
||||||
|
footnotes: false
|
||||||
|
}
|
||||||
|
|
||||||
|
# see: https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch
|
||||||
|
REDCARPET_RENDERER_OPTS = {
|
||||||
|
no_images: true
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize(text, allow_a: true, class_names_map: {})
|
||||||
|
@text = (text || "").gsub(/\R/, "\n\n") # force double \n otherwise a single one won't split paragraph
|
||||||
|
.split("\n\n") #
|
||||||
|
.map(&:lstrip) # this block prevent redcarpet to consider " text" as block code by lstriping
|
||||||
|
.join("\n\n") #
|
||||||
|
@allow_a = allow_a
|
||||||
|
@renderer = Redcarpet::Markdown.new(
|
||||||
|
Redcarpet::BareRenderer.new(link_attributes: external_link_attributes, class_names_map: class_names_map),
|
||||||
|
REDCARPET_EXTENSIONS.merge(autolink: @allow_a)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def external_link_attributes
|
||||||
|
{ target: '_blank', rel: 'noopener noreferrer' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def tags
|
||||||
|
if @allow_a
|
||||||
|
Rails.configuration.action_view.sanitized_allowed_tags + ['a']
|
||||||
|
else
|
||||||
|
Rails.configuration.action_view.sanitized_allowed_tags
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attributes
|
||||||
|
['target', 'rel', 'href', 'class']
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
= sanitize(@renderer.render(@text), tags:, attributes:)
|
|
@ -1,15 +0,0 @@
|
||||||
module StringToHtmlHelper
|
|
||||||
def string_to_html(str, wrapper_tag = 'p', allow_a: false)
|
|
||||||
return nil if str.blank?
|
|
||||||
html_formatted = simple_format(str, {}, { wrapper_tag: wrapper_tag })
|
|
||||||
with_links = Anchored::Linker.auto_link(html_formatted, target: '_blank', rel: 'noopener')
|
|
||||||
|
|
||||||
tags = if allow_a
|
|
||||||
Rails.configuration.action_view.sanitized_allowed_tags + ['a']
|
|
||||||
else
|
|
||||||
Rails.configuration.action_view.sanitized_allowed_tags
|
|
||||||
end
|
|
||||||
|
|
||||||
sanitize(with_links, tags:, attributes: ['target', 'rel', 'href'])
|
|
||||||
end
|
|
||||||
end
|
|
21
app/lib/redcarpet/bare_renderer.rb
Normal file
21
app/lib/redcarpet/bare_renderer.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
module Redcarpet
|
||||||
|
class BareRenderer < Redcarpet::Render::HTML
|
||||||
|
include ActionView::Helpers::TagHelper
|
||||||
|
|
||||||
|
# won't use rubocop tag method because it is missing output buffer
|
||||||
|
# rubocop:disable Rails/ContentTag
|
||||||
|
def list(content, list_type)
|
||||||
|
tag = list_type == :ordered ? :ol : :ul
|
||||||
|
content_tag(tag, content, { class: @options[:class_names_map].fetch(:list) {} }, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_item(content, list_type)
|
||||||
|
content_tag(:li, content.strip.gsub(/<\/?p>/, ''), {}, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def paragraph(text)
|
||||||
|
content_tag(:p, text, { class: @options[:class_names_map].fetch(:paragraph) {} }, false)
|
||||||
|
end
|
||||||
|
# rubocop:enable Rails/ContentTag
|
||||||
|
end
|
||||||
|
end
|
|
@ -35,4 +35,4 @@
|
||||||
- if avis.piece_justificative_file.attached?
|
- if avis.piece_justificative_file.attached?
|
||||||
= render Attachment::ShowComponent.new(attachment: avis.piece_justificative_file.attachment)
|
= render Attachment::ShowComponent.new(attachment: avis.piece_justificative_file.attachment)
|
||||||
.answer-body
|
.answer-body
|
||||||
= simple_format(avis.answer)
|
= render SimpleFormatComponent.new(avis.answer, allow_a: false)
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
- if @dossier.etablissement&.as_degraded_mode?
|
- if @dossier.etablissement&.as_degraded_mode?
|
||||||
.container
|
.container
|
||||||
= render Dsfr::CalloutComponent.new(title: "Données de l’entreprise non vérifiées", theme: :warning, icon: "fr-icon-feedback-fill") do |c|
|
= render Dsfr::CalloutComponent.new(title: "Données de l’entreprise non vérifiées", theme: :warning, icon: "fr-icon-feedback-fill") do |c|
|
||||||
- c.with_body do
|
- c.with_html_body do
|
||||||
Les services de l’INSEE sont indisponibles, nous ne pouvons pas
|
%p
|
||||||
vérifier les informations liées à l’établissement de ce dossier.
|
Les services de l’INSEE sont indisponibles, nous ne pouvons pas vérifier les informations liées à l’établissement de ce dossier.
|
||||||
%strong Il n’est pas possible d’accepter ou de refuser un dossier sans cette étape.
|
%strong
|
||||||
%br
|
Il n’est pas possible d’accepter ou de refuser un dossier sans cette étape.
|
||||||
%br
|
%p
|
||||||
Les informations sur l'entreprise arriveront d’ici quelques heures.
|
Les informations sur l'entreprise arriveront d’ici quelques heures.
|
||||||
|
|
||||||
= render partial: "shared/dossiers/demande", locals: { dossier: @dossier, demande_seen_at: @demande_seen_at, profile: 'instructeur' }
|
= render partial: "shared/dossiers/demande", locals: { dossier: @dossier, demande_seen_at: @demande_seen_at, profile: 'instructeur' }
|
||||||
|
|
|
@ -51,4 +51,4 @@
|
||||||
- if avis.piece_justificative_file.attached?
|
- if avis.piece_justificative_file.attached?
|
||||||
= render Attachment::ShowComponent.new(attachment: avis.piece_justificative_file.attachment)
|
= render Attachment::ShowComponent.new(attachment: avis.piece_justificative_file.attachment)
|
||||||
.answer-body
|
.answer-body
|
||||||
= simple_format(avis.answer)
|
= render SimpleFormatComponent.new(avis.answer, allow_a: false)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
= render Dsfr::CalloutComponent.new(title: t("views.prefill_descriptions.edit.json_description_title"), theme: :success, icon: "fr-icon-layout-grid-fill") do |c|
|
= render Dsfr::CalloutComponent.new(title: t("views.prefill_descriptions.edit.json_description_title"), theme: :success, icon: "fr-icon-layout-grid-fill") do |c|
|
||||||
- c.with_body do
|
- c.with_html_body do
|
||||||
= t("views.prefill_descriptions.edit.json_description_info")
|
%p= t("views.prefill_descriptions.edit.json_description_info")
|
||||||
%pre
|
%pre
|
||||||
%code.code-block
|
%code.code-block
|
||||||
= prefill_json_description_url(procedure.path)
|
= prefill_json_description_url(procedure.path)
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
- if prefill_description.prefilled_champs.any?
|
- if prefill_description.prefilled_champs.any?
|
||||||
= render Dsfr::CalloutComponent.new(title: t("views.prefill_descriptions.edit.prefill_link_title"), theme: theme, icon: icon) do |c|
|
= render Dsfr::CalloutComponent.new(title: t("views.prefill_descriptions.edit.prefill_link_title"), theme: theme, icon: icon) do |c|
|
||||||
- c.with_body do
|
- c.with_html_body do
|
||||||
= body
|
%p= body
|
||||||
%pre
|
%pre
|
||||||
%code.code-block
|
%code.code-block
|
||||||
= prefill_description.prefill_link
|
= prefill_description.prefill_link
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
- if prefill_description.prefilled_champs.any?
|
- if prefill_description.prefilled_champs.any?
|
||||||
|
|
||||||
= render Dsfr::CalloutComponent.new(title: t("views.prefill_descriptions.edit.prefill_query_title"), theme: :success, icon: "fr-icon-code-box-fill") do |c|
|
= render Dsfr::CalloutComponent.new(title: t("views.prefill_descriptions.edit.prefill_query_title"), theme: :success, icon: "fr-icon-code-box-fill") do |c|
|
||||||
- c.with_body do
|
- c.with_html_body do
|
||||||
= t("views.prefill_descriptions.edit.prefill_query_info")
|
%p= t("views.prefill_descriptions.edit.prefill_query_info")
|
||||||
%pre
|
%pre
|
||||||
%code.code-block
|
%code.code-block
|
||||||
= prefill_description.prefill_query
|
= prefill_description.prefill_query
|
||||||
|
|
|
@ -23,5 +23,5 @@
|
||||||
|
|
||||||
.procedure-description
|
.procedure-description
|
||||||
.procedure-description-body.read-more-enabled.read-more-collapsed{ tabindex: "0", role: "region", "aria-label": t('views.users.dossiers.identite.description') }
|
.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)
|
= h render SimpleFormatComponent.new(procedure.description, allow_a: true)
|
||||||
= button_tag "Afficher la description complète", class: 'button read-more-button'
|
= button_tag "Afficher la description complète", class: 'button read-more-button'
|
||||||
|
|
|
@ -6,11 +6,11 @@
|
||||||
Pour poser une question sur ce dossier, contactez :
|
Pour poser une question sur ce dossier, contactez :
|
||||||
%p
|
%p
|
||||||
= service.nom
|
= service.nom
|
||||||
%br
|
%p
|
||||||
= service.organisme
|
= service.organisme
|
||||||
%br
|
|
||||||
- horaires = "Horaires : #{formatted_horaires(service.horaires)}"
|
- if service.horaires.present?
|
||||||
= simple_format(horaires)
|
= render SimpleFormatComponent.new("Horaires : #{formatted_horaires(service.horaires)}")
|
||||||
%p
|
%p
|
||||||
= mail_to service.email,
|
= mail_to service.email,
|
||||||
service.email,
|
service.email,
|
||||||
|
|
|
@ -12,13 +12,16 @@
|
||||||
- elsif service.present?
|
- elsif service.present?
|
||||||
%li
|
%li
|
||||||
= link_to I18n.t('users.procedure_footer.contact.email.link', service_email: service.email), "mailto:#{service.email}", class: 'fr-footer__top-link'
|
= link_to I18n.t('users.procedure_footer.contact.email.link', service_email: service.email), "mailto:#{service.email}", class: 'fr-footer__top-link'
|
||||||
%li
|
- if service.telephone.present? || service.horaires.present?
|
||||||
- horaires = "#{I18n.t('users.procedure_footer.contact.schedule.prefix')}#{formatted_horaires(service.horaires)}"
|
%li
|
||||||
= link_to service.telephone_url, class: 'fr-footer__top-link' do
|
- horaires = "#{I18n.t('users.procedure_footer.contact.schedule.prefix')}#{formatted_horaires(service.horaires)}"
|
||||||
%p
|
- if service.telephone.present?
|
||||||
= I18n.t('users.procedure_footer.contact.phone.link', service_telephone: service.telephone)
|
= link_to service.telephone_url, class: 'fr-footer__top-link' do
|
||||||
%p
|
%p
|
||||||
= horaires
|
= I18n.t('users.procedure_footer.contact.phone.link', service_telephone: service.telephone)
|
||||||
|
- if service.horaires.present?
|
||||||
|
%p
|
||||||
|
= horaires
|
||||||
%li
|
%li
|
||||||
= link_to I18n.t('users.procedure_footer.contact.stats.link'), statistiques_path(procedure.path), class: 'fr-footer__top-link', rel: 'noopener'
|
= link_to I18n.t('users.procedure_footer.contact.stats.link'), statistiques_path(procedure.path), class: 'fr-footer__top-link', rel: 'noopener'
|
||||||
|
|
||||||
|
@ -65,13 +68,13 @@
|
||||||
|
|
||||||
- if service.present?
|
- if service.present?
|
||||||
.fr-footer__content
|
.fr-footer__content
|
||||||
|
|
||||||
%p.fr-footer__content-desc
|
%p.fr-footer__content-desc
|
||||||
= I18n.t('users.procedure_footer.managed_by.header')
|
= I18n.t('users.procedure_footer.managed_by.header')
|
||||||
%span{ lang: :fr }
|
%span{ lang: :fr }= "#{service.nom}, #{service.organisme},"
|
||||||
= "#{service.nom},"
|
%div{ lang: :fr }
|
||||||
= "#{service.organisme},"
|
= render SimpleFormatComponent.new(service.adresse, class_names_map: {paragraph: 'fr-footer__content-desc'})
|
||||||
= string_to_html(service.adresse, 'span')
|
= render partial: "shared/footer_content_list"
|
||||||
= render partial: "shared/footer_content_list"
|
|
||||||
|
|
||||||
.fr-footer__bottom
|
.fr-footer__bottom
|
||||||
= render partial: 'users/general_footer_row', locals: { dossier: dossier }
|
= render partial: 'users/general_footer_row', locals: { dossier: dossier }
|
||||||
|
|
73
spec/components/simple_format_component_spec.rb
Normal file
73
spec/components/simple_format_component_spec.rb
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
describe SimpleFormatComponent, type: :component do
|
||||||
|
let(:allow_a) { false }
|
||||||
|
before { render_inline(described_class.new(text, allow_a: allow_a)) }
|
||||||
|
|
||||||
|
context 'one line' do
|
||||||
|
let(:text) do
|
||||||
|
"1er paragraphe"
|
||||||
|
end
|
||||||
|
it { expect(page).to have_selector("p", count: 1, text: text) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'one with leading spaces' do
|
||||||
|
let(:text) do
|
||||||
|
<<-TEXT
|
||||||
|
1er paragraphe
|
||||||
|
TEXT
|
||||||
|
end
|
||||||
|
it { expect(page).to have_selector("p", count: 1, text: text.strip) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'two lines' do
|
||||||
|
let(:text) do
|
||||||
|
<<~TEXT
|
||||||
|
1er paragraphe
|
||||||
|
2eme paragraphe
|
||||||
|
TEXT
|
||||||
|
end
|
||||||
|
|
||||||
|
it { expect(page).to have_selector("p", count: 2) }
|
||||||
|
it { text.split("\n").map(&:strip).map { expect(page).to have_text(_1) } }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'unordered list items' do
|
||||||
|
let(:text) do
|
||||||
|
<<~TEXT
|
||||||
|
- 1er paragraphe
|
||||||
|
- paragraphe
|
||||||
|
TEXT
|
||||||
|
end
|
||||||
|
|
||||||
|
it { expect(page).to have_selector("ul", count: 1) }
|
||||||
|
it { expect(page).to have_selector("li", count: 2) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'ordered list items' do
|
||||||
|
let(:text) do
|
||||||
|
<<~TEXT
|
||||||
|
1. 1er paragraphe
|
||||||
|
2. paragraphe
|
||||||
|
TEXT
|
||||||
|
end
|
||||||
|
|
||||||
|
it { expect(page).to have_selector("ol", count: 1) }
|
||||||
|
it { expect(page).to have_selector("li", count: 2) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'auto-link' do
|
||||||
|
let(:text) do
|
||||||
|
<<~TEXT
|
||||||
|
bonjour https://www.demarches-simplifiees.fr
|
||||||
|
TEXT
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'enabled' do
|
||||||
|
let(:allow_a) { true }
|
||||||
|
it { expect(page).to have_selector("a") }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'disabled' do
|
||||||
|
it { expect(page).not_to have_selector("a") }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,49 +0,0 @@
|
||||||
RSpec.describe StringToHtmlHelper, type: :helper do
|
|
||||||
describe "#string_to_html" do
|
|
||||||
let(:allow_a) { false }
|
|
||||||
subject { string_to_html(description, allow_a:) }
|
|
||||||
|
|
||||||
context "with some simple texte" do
|
|
||||||
let(:description) { "1er ligne \n 2ieme ligne" }
|
|
||||||
|
|
||||||
it { is_expected.to eq("<p>1er ligne \n<br> 2ieme ligne</p>") }
|
|
||||||
end
|
|
||||||
|
|
||||||
context "with a link" do
|
|
||||||
context "using an authorized scheme" do
|
|
||||||
let(:description) { "Cliquez sur https://d-s.fr pour continuer." }
|
|
||||||
|
|
||||||
context 'with a tag authorized' do
|
|
||||||
let(:allow_a) { true }
|
|
||||||
it { is_expected.to eq("<p>Cliquez sur <a href=\"https://d-s.fr\" target=\"_blank\" rel=\"noopener\">https://d-s.fr</a> pour continuer.</p>") }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'without a tag' do
|
|
||||||
it { is_expected.to eq("<p>Cliquez sur https://d-s.fr pour continuer.</p>") }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "using a non-authorized scheme" do
|
|
||||||
let(:description) { "Cliquez sur file://etc/password pour continuer." }
|
|
||||||
it { is_expected.to eq("<p>Cliquez sur file://etc/password pour continuer.</p>") }
|
|
||||||
end
|
|
||||||
|
|
||||||
context "not actually an URL" do
|
|
||||||
let(:description) { "Pour info: il ne devrait y avoir aucun lien." }
|
|
||||||
it { is_expected.to eq("<p>Pour info: il ne devrait y avoir aucun lien.</p>") }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "with empty decription" do
|
|
||||||
let(:description) { nil }
|
|
||||||
|
|
||||||
it { is_expected.to eq nil }
|
|
||||||
end
|
|
||||||
|
|
||||||
context "with a bad script" do
|
|
||||||
let(:description) { '<script>bad</script>' }
|
|
||||||
|
|
||||||
it { is_expected.to eq('<p>bad</p>') }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -63,7 +63,7 @@ describe 'Inviting an expert:', js: true do
|
||||||
click_on 'Avis externes'
|
click_on 'Avis externes'
|
||||||
|
|
||||||
expect(page).to have_content(answered_avis.expert.email)
|
expect(page).to have_content(answered_avis.expert.email)
|
||||||
answered_avis.answer.split("\n").each do |answer_line|
|
answered_avis.answer.split("\n").map { |line| line.gsub("- ", "") }.map do |answer_line|
|
||||||
expect(page).to have_content(answer_line)
|
expect(page).to have_content(answer_line)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,7 +26,9 @@ describe 'instructeurs/shared/avis/_list.html.haml', type: :view do
|
||||||
let(:avis) { [create(:avis, :with_answer, claimant: instructeur, experts_procedure: experts_procedure)] }
|
let(:avis) { [create(:avis, :with_answer, claimant: instructeur, experts_procedure: experts_procedure)] }
|
||||||
|
|
||||||
it 'renders the answer formatted with newlines' do
|
it 'renders the answer formatted with newlines' do
|
||||||
expect(subject).to include(simple_format(avis.first.answer))
|
expect(subject).to have_selector(".answer-body p", text: avis.first.answer.split("\n").first)
|
||||||
|
expect(subject).to have_selector(".answer-body ul", count: 1) # avis.answer has two list item
|
||||||
|
expect(subject).to have_selector(".answer-body ul li", count: 2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue