From 49be3a797a6b521ef50702bb769e942ba92585d3 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 24 Jul 2024 17:14:06 +0200 Subject: [PATCH 1/7] refactor(input): don't duplicate hint logic --- app/components/dsfr/input_errorable.rb | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/components/dsfr/input_errorable.rb b/app/components/dsfr/input_errorable.rb index 9ae359fd86b..c3851b49714 100644 --- a/app/components/dsfr/input_errorable.rb +++ b/app/components/dsfr/input_errorable.rb @@ -127,6 +127,8 @@ def default_hint end end + def hint? = hint.present? + def password? false end @@ -142,15 +144,6 @@ def autoresize? def hintable? false end - - def hint? - return true if get_slot(:hint).present? - - maybe_hint = I18n.exists?("activerecord.attributes.#{object.class.name.underscore}.hints.#{@attribute}") - maybe_hint_html = I18n.exists?("activerecord.attributes.#{object.class.name.underscore}.hints.#{@attribute}_html") - - maybe_hint || maybe_hint_html - end end end end From ff62e99e7be81fb694cfb431551871bcb97cc793 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 24 Jul 2024 17:18:02 +0200 Subject: [PATCH 2/7] refactor(contact): suggest email correction, strict email validation, fix admin form --- app/controllers/support_controller.rb | 82 +++++++------- app/helpers/application_helper.rb | 2 +- app/jobs/helpscout_create_conversation_job.rb | 31 +++++- .../helpscout/{form_adapter.rb => form.rb} | 97 ++++++++-------- app/views/support/_form.html.haml | 58 ++++++++++ app/views/support/admin.html.haml | 47 +------- app/views/support/index.html.haml | 2 +- config/i18n-tasks.yml | 1 + config/locales/en.yml | 3 - config/locales/fr.yml | 3 - config/locales/views/support/en.yml | 66 +++++++---- config/locales/views/support/fr.yml | 65 +++++++---- spec/controllers/support_controller_spec.rb | 72 +++++++++--- .../helpscout_create_conversation_job_spec.rb | 69 ++++++++---- spec/lib/helpscout/form_adapter_spec.rb | 104 ------------------ 15 files changed, 371 insertions(+), 331 deletions(-) rename app/lib/helpscout/{form_adapter.rb => form.rb} (51%) create mode 100644 app/views/support/_form.html.haml delete mode 100644 spec/lib/helpscout/form_adapter_spec.rb diff --git a/app/controllers/support_controller.rb b/app/controllers/support_controller.rb index e2e536499d8..f967f6fa25e 100644 --- a/app/controllers/support_controller.rb +++ b/app/controllers/support_controller.rb @@ -2,11 +2,11 @@ class SupportController < ApplicationController invisible_captcha only: [:create], on_spam: :redirect_to_root def index - setup_context + @form = Helpscout::Form.new(tags: tags_from_query_params, dossier_id: dossier&.id, current_user:) end def admin - setup_context_admin + @form = Helpscout::Form.new(tags: tags_from_query_params, current_user:, for_admin: true) end def create @@ -17,85 +17,79 @@ def create return end - create_conversation_later - flash.notice = "Votre message a été envoyé." + @form = Helpscout::Form.new(support_form_params.except(:piece_jointe).merge(current_user:)) - if params[:admin] - redirect_to root_path(formulaire_contact_admin_submitted: true) + if @form.valid? + create_conversation_later(@form) + flash.notice = "Votre message a été envoyé." + + redirect_to root_path else - redirect_to root_path(formulaire_contact_general_submitted: true) + flash.alert = @form.errors.full_messages + render @form.for_admin ? :admin : :index end end private - def setup_context - @dossier_id = dossier&.id - @tags = tags - @options = Helpscout::FormAdapter.options - end - - def setup_context_admin - @tags = tags - @options = Helpscout::FormAdapter.admin_options - end - - def create_conversation_later - if params[:piece_jointe] + def create_conversation_later(form) + if support_form_params[:piece_jointe].present? blob = ActiveStorage::Blob.create_and_upload!( - io: params[:piece_jointe].tempfile, - filename: params[:piece_jointe].original_filename, - content_type: params[:piece_jointe].content_type, + io: support_form_params[:piece_jointe].tempfile, + filename: support_form_params[:piece_jointe].original_filename, + content_type: support_form_params[:piece_jointe].content_type, identify: false ).tap(&:scan_for_virus_later) end HelpscoutCreateConversationJob.perform_later( blob_id: blob&.id, - subject: params[:subject], - email: email, - phone: params[:phone], - text: params[:text], - dossier_id: dossier&.id, + subject: form.subject, + email: current_user&.email || form.email, + phone: form.phone, + text: form.text, + dossier_id: form.dossier_id, browser: browser_name, - tags: tags + tags: form.tags_array ) end def create_commentaire attributes = { - piece_jointe: params[:piece_jointe], - body: "[#{params[:subject]}]

#{params[:text]}" + piece_jointe: support_form_params[:piece_jointe], + body: "[#{support_form_params[:subject]}]

#{support_form_params[:text]}" } CommentaireService.create!(current_user, dossier, attributes) end - def tags - [params[:tags], params[:type]].flatten.compact - .map { |tag| tag.split(',') } - .flatten - .compact_blank.uniq - end - def browser_name if browser.known? "#{browser.name} #{browser.version} (#{browser.platform.name})" end end - def direct_message? - user_signed_in? && params[:type] == Helpscout::FormAdapter::TYPE_INSTRUCTION && dossier.present? && dossier.messagerie_available? + def tags_from_query_params + support_form_params[:tags]&.join(",") || "" end - def dossier - @dossier ||= current_user&.dossiers&.find_by(id: params[:dossier_id]) + def direct_message? + user_signed_in? && support_form_params[:type] == Helpscout::Form::TYPE_INSTRUCTION && dossier.present? && dossier.messagerie_available? end - def email - current_user&.email || params[:email] + def dossier + @dossier ||= current_user&.dossiers&.find_by(id: support_form_params[:dossier_id]) end def redirect_to_root redirect_to root_path, alert: t('invisible_captcha.sentence_for_humans') end + + def support_form_params + keys = [:email, :subject, :text, :type, :dossier_id, :piece_jointe, :phone, :tags, :for_admin] + if params.key?(:helpscout_form) # submitting form + params.require(:helpscout_form).permit(*keys) + else + params.permit(:dossier_id, tags: []) # prefilling form + end + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 177d5196ed3..3cfe2b1c152 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -74,7 +74,7 @@ def contact_link(title, options = {}) tags, type, dossier_id = options.values_at(:tags, :type, :dossier_id) options.except!(:tags, :type, :dossier_id) - params = { tags: tags, type: type, dossier_id: dossier_id }.compact + params = { tags: Array.wrap(tags), type: type, dossier_id: dossier_id }.compact link_to title, contact_url(params), options end diff --git a/app/jobs/helpscout_create_conversation_job.rb b/app/jobs/helpscout_create_conversation_job.rb index a8175d0294c..068fe651130 100644 --- a/app/jobs/helpscout_create_conversation_job.rb +++ b/app/jobs/helpscout_create_conversation_job.rb @@ -8,7 +8,9 @@ class FileNotScannedYetError < StandardError retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10 - def perform(blob_id: nil, **args) + attr_reader :api + + def perform(blob_id: nil, **params) if blob_id.present? blob = ActiveStorage::Blob.find(blob_id) raise FileNotScannedYetError if blob.virus_scanner.pending? @@ -16,6 +18,31 @@ def perform(blob_id: nil, **args) blob = nil unless blob.virus_scanner.safe? end - Helpscout::FormAdapter.new(**args, blob:).send_form + @api = Helpscout::API.new + + create_conversation(params, blob) + end + + private + + def create_conversation(params, blob) + response = api.create_conversation( + params[:email], + params[:subject], + params[:text], + blob + ) + + if response.success? + conversation_id = response.headers['Resource-ID'] + + if params[:phone].present? + api.add_phone_number(params[:email], params[:phone]) + end + + api.add_tags(conversation_id, params[:tags]) + else + fail "Error while creating conversation: #{response.response_code} '#{response.body}'" + end end end diff --git a/app/lib/helpscout/form_adapter.rb b/app/lib/helpscout/form.rb similarity index 51% rename from app/lib/helpscout/form_adapter.rb rename to app/lib/helpscout/form.rb index 4127e9dc252..647c4e2a170 100644 --- a/app/lib/helpscout/form_adapter.rb +++ b/app/lib/helpscout/form.rb @@ -1,7 +1,39 @@ -class Helpscout::FormAdapter - attr_reader :params +class Helpscout::Form + include ActiveModel::Model + include ActiveModel::Attributes - def self.options + attribute :email, :string + attribute :subject, :string + attribute :text, :string + attribute :type, :string + attribute :dossier_id, :integer + attribute :tags, :string + attribute :phone, :string + attribute :tags, :string + attribute :for_admin, :boolean, default: false + + validates :email, presence: true, strict_email: true, if: :require_email? # i18n-tasks-use t('activemodel.errors.models.helpscout/form.invalid_email_format') + validates :subject, presence: true + validates :text, presence: true + validates :type, presence: true + + attr_reader :current_user + attr_reader :options + + TYPE_INFO = 'procedure_info' + TYPE_PERDU = 'lost_user' + TYPE_INSTRUCTION = 'instruction_info' + TYPE_AMELIORATION = 'product' + TYPE_AUTRE = 'other' + + ADMIN_TYPE_RDV = 'admin_demande_rdv' + ADMIN_TYPE_QUESTION = 'admin_question' + ADMIN_TYPE_SOUCIS = 'admin_soucis' + ADMIN_TYPE_PRODUIT = 'admin_suggestion_produit' + ADMIN_TYPE_DEMANDE_COMPTE = 'admin_demande_compte' + ADMIN_TYPE_AUTRE = 'admin_autre' + + def self.default_options [ [I18n.t(:question, scope: [:support, :index, TYPE_INFO]), TYPE_INFO, I18n.t("links.common.faq.contacter_service_en_charge_url")], [I18n.t(:question, scope: [:support, :index, TYPE_PERDU]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL], @@ -22,60 +54,25 @@ def self.admin_options ] end - def initialize(params = {}, api = nil) - @params = params - @api = api || Helpscout::API.new - end - - TYPE_INFO = 'procedure_info' - TYPE_PERDU = 'lost_user' - TYPE_INSTRUCTION = 'instruction_info' - TYPE_AMELIORATION = 'product' - TYPE_AUTRE = 'other' - - ADMIN_TYPE_RDV = 'admin demande rdv' - ADMIN_TYPE_QUESTION = 'admin question' - ADMIN_TYPE_SOUCIS = 'admin soucis' - ADMIN_TYPE_PRODUIT = 'admin suggestion produit' - ADMIN_TYPE_DEMANDE_COMPTE = 'admin demande compte' - ADMIN_TYPE_AUTRE = 'admin autre' + def initialize(params) + @current_user = params.delete(:current_user) + params[:email] = EmailSanitizableConcern::EmailSanitizer.sanitize(params[:email]) if params[:email].present? + super(params) - def send_form - conversation_id = create_conversation - - if conversation_id.present? - add_tags(conversation_id) - true + @options = if for_admin? + self.class.admin_options else - false + self.class.default_options end end - private - - def add_tags(conversation_id) - @api.add_tags(conversation_id, tags) - end + alias for_admin? for_admin - def tags - (params[:tags].presence || []) + ['contact form'] + def tags_array + (tags&.split(",") || []) + ['contact form', type] end - def create_conversation - response = @api.create_conversation( - params[:email], - params[:subject], - params[:text], - params[:blob] - ) + def require_email? = current_user.blank? - if response.success? - if params[:phone].present? - @api.add_phone_number(params[:email], params[:phone]) - end - response.headers['Resource-ID'] - else - raise StandardError, "Error while creating conversation: #{response.response_code} '#{response.body}'" - end - end + def persisted? = false end diff --git a/app/views/support/_form.html.haml b/app/views/support/_form.html.haml new file mode 100644 index 00000000000..bbe8747fea7 --- /dev/null +++ b/app/views/support/_form.html.haml @@ -0,0 +1,58 @@ += form_for form, url: contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :support } do |f| + %p.fr-hint-text= t('asterisk_html', scope: [:utils]) + + - if form.require_email? + = render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email' }) do |c| + - c.with_label { Helpscout::Form.human_attribute_name(form.for_admin? ? :email_pro : :email) } + + %fieldset.fr-fieldset{ name: "type" } + %legend.fr-fieldset__legend.fr-fieldset__legend--regular + = t('.your_question') + = render EditableChamp::AsteriskMandatoryComponent.new + .fr-fieldset__content + - form.options.each do |(question, question_type, link)| + .fr-radio-group + = f.radio_button :type, question_type, required: true, data: {"support-target": "inputRadio" }, checked: question_type == form.type + = f.label "type_#{question_type}", { 'aria-controls': link ? "card-#{question_type}" : nil, class: 'fr-label' } do + = question + + - if link.present? + .fr-ml-3w{ id: "card-#{question_type}", + class: class_names('hidden' => question_type != form.type), + "aria-hidden": question_type != form.type, + data: { "support-target": "content" } } + = render Dsfr::CalloutComponent.new(title: t('.our_answer')) do |c| + - c.with_html_body do + -# i18n-tasks-use t("support.index.#{question_type}.answer_html") + = t('answer_html', scope: [:support, :index, question_type], base_url: Current.application_base_url, "link_#{question_type}": link) + + + - if form.for_admin? + = render Dsfr::InputComponent.new(form: f, attribute: :phone, required: false) + - else + = render Dsfr::InputComponent.new(form: f, attribute: :dossier_id, required: false) + + = render Dsfr::InputComponent.new(form: f, attribute: :subject) + + = render Dsfr::InputComponent.new(form: f, attribute: :text, input_type: :text_area, opts: { rows: 6 }) + + - if !form.for_admin? + .fr-upload-group + = f.label :piece_jointe, class: 'fr-label' do + = t('pj', scope: [:utils]) + %span.fr-hint-text + = t('.notice_upload_group') + + %p.notice.hidden{ data: { 'contact-type-only': Helpscout::Form::TYPE_AMELIORATION } } + = t('.notice_pj_product') + %p.notice.hidden{ data: { 'contact-type-only': Helpscout::Form::TYPE_AUTRE } } + = t('.notice_pj_other') + = f.file_field :piece_jointe, class: 'fr-upload', accept: '.jpg, .jpeg, .png, .pdf' + + = f.hidden_field :tags + = f.hidden_field :for_admin + + = invisible_captcha + + .fr-input-group.fr-my-3w + = f.submit t('send_mail', scope: [:utils]), type: :submit, class: 'fr-btn', data: { disable: true } diff --git a/app/views/support/admin.html.haml b/app/views/support/admin.html.haml index 28a53bc7243..1771256fc49 100644 --- a/app/views/support/admin.html.haml +++ b/app/views/support/admin.html.haml @@ -1,49 +1,12 @@ -- content_for(:title, 'Contact') +- content_for(:title, t('.contact_team')) +- content_for :footer do + = render partial: "root/footer" #contact-form .fr-container %h1 = t('.contact_team') - .description - = t('.admin_intro_html', contact_path: contact_path) - %br - %p.mandatory-explanation= t('asterisk_html', scope: [:utils]) + .fr-highlight= t('.admin_intro_html', contact_path: contact_path) - = form_tag contact_path, method: :post, class: 'form' do |f| - - if !user_signed_in? - .contact-champ - = label_tag :email do - = t('.pro_mail') - %span.mandatory * - = text_field_tag :email, params[:email], required: true - - .contact-champ - = label_tag :type do - = t('.your_question') - %span.mandatory * - = select_tag :type, options_for_select(@options, params[:type]) - - .contact-champ - = label_tag :phone do - = t('.pro_phone_number') - = text_field_tag :phone - - .contact-champ - = label_tag :subject do - = t('subject', scope: [:utils]) - = text_field_tag :subject, params[:subject], required: false - - .contact-champ - = label_tag :text do - = t('message', scope: [:utils]) - %span.mandatory * - = text_area_tag :text, params[:text], rows: 6, required: true - - = invisible_captcha - - = hidden_field_tag :tags, @tags&.join(',') - = hidden_field_tag :admin, true - - .send-wrapper - = button_tag t('send_mail', scope: [:utils]), type: :submit, class: 'button send primary' + = render partial: "form", object: @form diff --git a/app/views/support/index.html.haml b/app/views/support/index.html.haml index 26727bcbeb3..237e5fa86b6 100644 --- a/app/views/support/index.html.haml +++ b/app/views/support/index.html.haml @@ -7,7 +7,7 @@ %h1 = t('.contact') - = form_tag contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :support } do + .fr-highlight= t('.intro_html') .description .recommandations diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 82d29a1c910..f2a6e6cc65d 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -102,6 +102,7 @@ ignore_unused: - 'activerecord.models.*' - 'activerecord.attributes.*' - 'activemodel.attributes.map_filter.*' +- 'activemodel.attributes.helpscout/form.*' - 'activerecord.errors.*' - 'errors.messages.blank' - 'errors.messages.content_type_invalid' diff --git a/config/locales/en.yml b/config/locales/en.yml index 9737fcab837..be6edf0fc52 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -52,9 +52,6 @@ en: asterisk_html: "Fields marked by an asterisk ( required ) are mandatory." mandatory_champs: All fields are mandatory. no_mandatory: (optional) - file_number: File number - subject: Subject - message: Message send_mail: Send message new_tab: New tab helpers: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2c6469d62e4..dfcb5d7b816 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -43,9 +43,6 @@ fr: asterisk_html: "Les champs suivis d’un astérisque ( obligatoire ) sont obligatoires." mandatory_champs: Tous les champs sont obligatoires. no_mandatory: (facultatif) - file_number: Numéro de dossier - subject: Sujet - message: Message send_mail: Envoyer le message new_tab: "Nouvel onglet" helpers: diff --git a/config/locales/views/support/en.yml b/config/locales/views/support/en.yml index 97012927680..6378df37e29 100644 --- a/config/locales/views/support/en.yml +++ b/config/locales/views/support/en.yml @@ -1,4 +1,19 @@ en: + activemodel: + attributes: + helpscout/form: + email: 'Your email address' + email_pro: Professional email address + phone: Professional phone number (direct line) + subject: Subject + text: Message + dossier_id: File number + hints: + email: 'Example: address@mail.com' + errors: + models: + helpscout/form: + invalid_email_format: 'is not valid' support: index: contact: Contact @@ -10,44 +25,51 @@ en: our_answer: Our answer notice_pj_product: A screenshot can help us identify the element to improve. notice_pj_other: A screenshot can help us identify the issue. - notice_upload_group: "Maximum size: 200 MB. Supported formats: jpg, png, pdf." + notice_upload_group: 'Maximum size: 200 MB. Supported formats: jpg, png, pdf.' + index: + contact: Contact + intro_html: + '

Contact us via this form and we will answer you as quickly as possible.

+

Make sure you provide all the required information so we can help you in the best way.

' procedure_info: question: I've encountered a problem while completing my application - answer_html: "

Are you sure that all the mandatory fields ( * ) are properly filled? -

If you have questions about the information requested, contact the service in charge of the procedure (FAQ).

" + answer_html: + '

Are you sure that all the mandatory fields ( * ) are properly filled? +

If you have questions about the information requested, contact the service in charge of the procedure (FAQ).

' instruction_info: question: I have a question about the instruction of my application - answer_html: "

If you have questions about the instruction of your application (response delay for example), contact directly the instructor via our mail system (FAQ).

-

If you are facing technical issues on the website, use the form below. We will not be able to inform you about the instruction of your application.

" + answer_html: + '

If you have questions about the instruction of your application (response delay for example), contact directly the instructor via our mail system (FAQ).

+

If you are facing technical issues on the website, use the form below. We will not be able to inform you about the instruction of your application.

' product: question: I have an idea to improve the website - answer_html: "

Got an idea? Please check our enhancement dashboard :

- " + answer_html: + '

Got an idea? Please check our enhancement dashboard :

+ ' lost_user: question: I am having trouble finding the procedure I am looking for - answer_html: "

We invite you to contact the administration in charge of the procedure so they can provide you the link. - It should look like this: %{base_url}/commencer/NOM_DE_LA_DEMARCHE.

-

You can find here the most popular procedures (licence, detr, etc.).

" + answer_html: + '

We invite you to contact the administration in charge of the procedure so they can provide you the link. + It should look like this: %{base_url}/commencer/NOM_DE_LA_DEMARCHE.

+

You can find here the most popular procedures (licence, detr, etc.).

' other: question: Other topic admin: - your_question: Your question - admin_intro_html: "

As an administration, you can contact us through this form. We'll answer you as quickly as possibly by e-mail or phone.

+ admin_intro_html: + '

As an administration, you can contact us through this form. We''ll answer you as quickly as possibly by e-mail or phone.

Caution, this form is dedicated to public bodies only. - It does not concern individuals, companies nor associations (except those recognised of public utility). If you belong to one of these categories, contact us here.

" + It does not concern individuals, companies nor associations (except those recognised of public utility). If you belong to one of these categories, contact us here.

' contact_team: Contact our team - pro_phone_number: Professional phone number (direct line) - pro_mail: Professional email address - admin question: + admin_question: question: I have a question about %{app_name} - admin demande rdv: + admin_demande_rdv: question: I request an appointment for an online presentation of %{app_name} - admin soucis: + admin_soucis: question: I am facing a technical issue on %{app_name} - admin suggestion produit: + admin_suggestion_produit: question: I have a suggestion for an evolution - admin demande compte: + admin_demande_compte: question: I want to open an admin account with an Orange, Wanadoo, etc. email - admin autre: + admin_autre: question: Other topic diff --git a/config/locales/views/support/fr.yml b/config/locales/views/support/fr.yml index 02f520625f9..eb6f74a5808 100644 --- a/config/locales/views/support/fr.yml +++ b/config/locales/views/support/fr.yml @@ -1,4 +1,19 @@ fr: + activemodel: + attributes: + helpscout/form: + email: 'Votre adresse email' + email_pro: Votre adresse email professionnelle + phone: Numéro de téléphone professionnel (ligne directe) + subject: Sujet + text: Message + dossier_id: Numéro de dossier + hints: + email: 'Exemple: adresse@mail.com' + errors: + models: + helpscout/form: + invalid_email_format: 'est invalide' support: index: contact: Contact @@ -10,44 +25,52 @@ fr: our_answer: Notre réponse notice_pj_product: Une capture d’écran peut nous aider à identifier plus facilement l’endroit à améliorer. notice_pj_other: Une capture d’écran peut nous aider à identifier plus facilement le problème. - notice_upload_group: "Taille maximale : 200 Mo. Formats supportés : jpg, png, pdf." + notice_upload_group: 'Taille maximale : 200 Mo. Formats supportés : jpg, png, pdf.' + index: + contact: Contact + intro_html: + '

Contactez-nous via ce formulaire et nous vous répondrons dans les plus brefs délais.

+

Pensez bien à nous donner le plus d’informations possible pour que nous puissions vous aider au mieux.

' procedure_info: question: J’ai un problème lors du remplissage de mon dossier - answer_html: "

Avez-vous bien vérifié que tous les champs obligatoires ( * ) sont remplis ? -

Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche (FAQ).

" + answer_html: + '

Avez-vous bien vérifié que tous les champs obligatoires ( * ) sont remplis ? +

Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche (FAQ).

' instruction_info: question: J’ai une question sur l’instruction de mon dossier - answer_html: "

Si vous avez des questions sur l’instruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie (FAQ).

-

Si vous souhaitez poser une question pour un problème technique sur le site, utilisez le formulaire ci-dessous. Nous ne pourrons pas vous renseigner sur l’instruction de votre dossier.

" + answer_html: + '

Si vous avez des questions sur l’instruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie (FAQ).

+

Si vous souhaitez poser une question pour un problème technique sur le site, utilisez le formulaire ci-dessous. Nous ne pourrons pas vous renseigner sur l’instruction de votre dossier.

' product: question: J’ai une idée d’amélioration pour votre site - answer_html: "

Une idée ? Pensez à consulter notre tableau de bord des améliorations :

+ answer_html: + '

Une idée ? Pensez à consulter notre tableau de bord des améliorations :

" +
  • Proposez votre propre idée.
  • ' lost_user: question: Je ne trouve pas la démarche que je veux faire - answer_html: "

    Nous vous invitons à contacter l’administration en charge de votre démarche pour qu’elle vous indique le lien à suivre. Celui-ci devrait ressembler à cela : %{base_url}/commencer/NOM_DE_LA_DEMARCHE.

    -

    Vous pouvez aussi consulter ici la liste de nos démarches les plus fréquentes (permis, detr, etc.).

    " + answer_html: + '

    Nous vous invitons à contacter l’administration en charge de votre démarche pour qu’elle vous indique le lien à suivre. Celui-ci devrait ressembler à cela : %{base_url}/commencer/NOM_DE_LA_DEMARCHE.

    +

    Vous pouvez aussi consulter ici la liste de nos démarches les plus fréquentes (permis, detr, etc.).

    ' other: question: Autre sujet + admin: - your_question: Votre question - admin_intro_html: "

    En tant qu’administration, vous pouvez nous contactez via ce formulaire. Nous vous répondrons dans les plus brefs délais, par email ou par téléphone.

    + admin_intro_html: + '

    En tant qu’administration, vous pouvez nous contactez via ce formulaire. Nous vous répondrons dans les plus brefs délais, par email ou par téléphone.

    Attention, ce formulaire est réservé uniquement aux organismes publics. - Il ne concerne ni les particuliers, ni les entreprises, ni les associations (sauf celles reconnues d’utilité publique). Si c'est votre cas, rendez-vous sur notre - formulaire de contact public.

    " + Il ne concerne ni les particuliers, ni les entreprises, ni les associations (sauf celles reconnues d’utilité publique). Si c''est votre cas, rendez-vous sur notre + formulaire de contact public.

    ' contact_team: Contactez notre équipe - pro_phone_number: Numéro de téléphone professionnel (ligne directe) - pro_mail: Adresse e-mail professionnelle - admin question: + admin_question: question: J’ai une question sur %{app_name} - admin demande rdv: + admin_demande_rdv: question: Demande de RDV pour une présentation à distance de %{app_name} - admin soucis: + admin_soucis: question: J’ai un problème technique avec %{app_name} - admin suggestion produit: + admin_suggestion_produit: question: J’ai une proposition d’évolution - admin demande compte: + admin_demande_compte: question: Je souhaite ouvrir un compte administrateur avec un email Orange, Wanadoo, etc. - admin autre: + admin_autre: question: Autre sujet diff --git a/spec/controllers/support_controller_spec.rb b/spec/controllers/support_controller_spec.rb index 281c7e4a3dc..55915b0f124 100644 --- a/spec/controllers/support_controller_spec.rb +++ b/spec/controllers/support_controller_spec.rb @@ -12,7 +12,7 @@ get :index expect(response.status).to eq(200) - expect(response.body).not_to have_content("Email *") + expect(response.body).not_to have_content("Votre adresse email") end describe "with dossier" do @@ -51,29 +51,29 @@ describe "send form" do subject do - post :create, params: params + post :create, params: { helpscout_form: params } end context "when invisible captcha is ignored" do - let(:params) { { subject: 'bonjour', text: 'un message' } } + let(:params) { { subject: 'bonjour', text: 'un message', type: 'procedure_info' } } it 'creates a conversation on HelpScout' do expect { subject }.to \ change(Commentaire, :count).by(0).and \ - have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params)) + have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:type))) expect(flash[:notice]).to match('Votre message a été envoyé.') - expect(response).to redirect_to root_path(formulaire_contact_general_submitted: true) + expect(response).to redirect_to root_path end context 'when a drafted dossier is mentionned' do let(:dossier) { create(:dossier) } let(:user) { dossier.user } - subject do - post :create, params: { + let(:params) do + { dossier_id: dossier.id, - type: Helpscout::FormAdapter::TYPE_INSTRUCTION, + type: Helpscout::Form::TYPE_INSTRUCTION, subject: 'bonjour', text: 'un message' } @@ -85,7 +85,7 @@ have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(subject: 'bonjour', dossier_id: dossier.id)) expect(flash[:notice]).to match('Votre message a été envoyé.') - expect(response).to redirect_to root_path(formulaire_contact_general_submitted: true) + expect(response).to redirect_to root_path end end @@ -93,10 +93,10 @@ let(:dossier) { create(:dossier, :en_construction) } let(:user) { dossier.user } - subject do - post :create, params: { + let(:params) do + { dossier_id: dossier.id, - type: Helpscout::FormAdapter::TYPE_INSTRUCTION, + type: Helpscout::Form::TYPE_INSTRUCTION, subject: 'bonjour', text: 'un message' } @@ -118,7 +118,13 @@ end context "when invisible captcha is filled" do - let(:params) { { subject: 'bonjour', text: 'un message', InvisibleCaptcha.honeypots.sample => 'boom' } } + subject do + post :create, params: { + helpscout_form: { subject: 'bonjour', text: 'un message', type: 'procedure_info' }, + InvisibleCaptcha.honeypots.sample => 'boom' + } + end + it 'does not create a conversation on HelpScout' do expect { subject }.not_to change(Commentaire, :count) expect(flash[:alert]).to eq(I18n.t('invisible_captcha.sentence_for_humans')) @@ -133,7 +139,7 @@ get :index expect(response.status).to eq(200) - expect(response.body).to have_text("Email") + expect(response.body).to have_text("Votre adresse email") end end @@ -147,18 +153,46 @@ expect(response.body).to include(tag) end end + + describe 'send form' do + subject do + post :create, params: { helpscout_form: params } + end + + let(:params) { { subject: 'bonjour', email: "me@rspec.net", text: 'un message', type: 'procedure_info' } } + + it 'creates a conversation on HelpScout' do + expect { subject }.to \ + change(Commentaire, :count).by(0).and \ + have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:type))) + + expect(flash[:notice]).to match('Votre message a été envoyé.') + expect(response).to redirect_to root_path + end + + context "when email is invalid" do + let(:params) { super().merge(email: "me@rspec") } + + it 'creates a conversation on HelpScout' do + expect { subject }.not_to have_enqueued_job(HelpscoutCreateConversationJob) + expect(response.body).to include("Le champ « Votre adresse email » est invalide") + expect(response.body).to include("bonjour") + expect(response.body).to include("un message") + end + end + end end context 'contact admin' do subject do - post :create, params: params + post :create, params: { helpscout_form: params } end - let(:params) { { admin: "true", email: "email@pro.fr", subject: 'bonjour', text: 'un message' } } + let(:params) { { for_admin: "true", email: "email@pro.fr", subject: 'bonjour', text: 'un message', type: 'admin question', phone: '06' } } describe "when form is filled" do it "creates a conversation on HelpScout" do - expect { subject }.to have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:admin))) + expect { subject }.to have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:for_admin, :type))) expect(flash[:notice]).to match('Votre message a été envoyé.') end @@ -176,7 +210,9 @@ end describe "when invisible captcha is filled" do - let(:params) { super().merge(InvisibleCaptcha.honeypots.sample => 'boom') } + subject do + post :create, params: { helpscout_form: params, InvisibleCaptcha.honeypots.sample => 'boom' } + end it 'does not create a conversation on HelpScout' do subject diff --git a/spec/jobs/helpscout_create_conversation_job_spec.rb b/spec/jobs/helpscout_create_conversation_job_spec.rb index 1220e538d64..1aa36a5d467 100644 --- a/spec/jobs/helpscout_create_conversation_job_spec.rb +++ b/spec/jobs/helpscout_create_conversation_job_spec.rb @@ -1,16 +1,43 @@ require 'rails_helper' RSpec.describe HelpscoutCreateConversationJob, type: :job do - let(:args) { { email: 'sender@email.com' } } + let(:api) { instance_double("Helpscout::API") } + let(:email) { 'help@rspec.net' } + let(:subject_text) { 'Bonjour' } + let(:text) { "J'ai un pb" } + let(:tags) { ["first tag"] } + let(:phone) { nil } + let(:params) { + { + email:, + subject: subject_text, + text:, + tags:, + phone: + } + } describe '#perform' do + before do + allow(Helpscout::API).to receive(:new).and_return(api) + allow(api).to receive(:create_conversation) + .and_return(double( + success?: true, + headers: { 'Resource-ID' => 'new-conversation-id' } + )) + allow(api).to receive(:add_tags) + allow(api).to receive(:add_phone_number) if params[:phone].present? + end + + subject { + described_class.perform_now(**params) + } + context 'when blob_id is not present' do it 'sends the form without a file' do - form_adapter = double('Helpscout::FormAdapter') - allow(Helpscout::FormAdapter).to receive(:new).with(hash_including(args.merge(blob: nil))).and_return(form_adapter) - expect(form_adapter).to receive(:send_form) - - described_class.perform_now(**args) + subject + expect(api).to have_received(:create_conversation).with(email, subject_text, text, nil) + expect(api).to have_received(:add_tags).with("new-conversation-id", tags) end end @@ -18,9 +45,11 @@ let(:blob) { ActiveStorage::Blob.create_and_upload!(io: StringIO.new("toto"), filename: "toto.png") } + let(:params) { super().merge(blob_id: blob.id) } before do allow(blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: pending, safe?: safe)) + allow(ActiveStorage::Blob).to receive(:find).with(blob.id).and_return(blob) end context 'when the file has not been scanned yet' do @@ -28,9 +57,7 @@ let(:safe) { false } it 'reenqueue job' do - expect { - described_class.perform_now(blob_id: blob.id, **args) - }.to have_enqueued_job(described_class).with(blob_id: blob.id, **args) + expect { subject }.to have_enqueued_job(described_class).with(params) end end @@ -39,11 +66,8 @@ let(:safe) { true } it 'downloads the file and sends the form' do - form_adapter = double('Helpscout::FormAdapter') - allow(Helpscout::FormAdapter).to receive(:new).with(hash_including(args.merge(blob:))).and_return(form_adapter) - allow(form_adapter).to receive(:send_form) - - described_class.perform_now(blob_id: blob.id, **args) + subject + expect(api).to have_received(:create_conversation).with(email, subject_text, text, blob) end end @@ -51,14 +75,19 @@ let(:pending) { false } let(:safe) { false } - it 'downloads the file and sends the form' do - form_adapter = double('Helpscout::FormAdapter') - allow(Helpscout::FormAdapter).to receive(:new).with(hash_including(args.merge(blob: nil))).and_return(form_adapter) - allow(form_adapter).to receive(:send_form) - - described_class.perform_now(blob_id: blob.id, **args) + it 'ignore the file' do + subject + expect(api).to have_received(:create_conversation).with(email, subject_text, text, nil) end end end + + context 'with a phone' do + let(:phone) { "06" } + it 'associates the phone number' do + subject + expect(api).to have_received(:add_phone_number).with(email, phone) + end + end end end diff --git a/spec/lib/helpscout/form_adapter_spec.rb b/spec/lib/helpscout/form_adapter_spec.rb deleted file mode 100644 index 8eaa085a622..00000000000 --- a/spec/lib/helpscout/form_adapter_spec.rb +++ /dev/null @@ -1,104 +0,0 @@ -describe Helpscout::FormAdapter do - describe '#send_form' do - let(:api) { spy(double(:api)) } - - context 'create_conversation' do - before do - allow(api).to receive(:create_conversation) - .and_return(double(success?: true, headers: {})) - described_class.new(params, api).send_form - end - - let(:params) { - { - email: email, - subject: subject, - text: text - } - } - let(:email) { 'paul.chavard@beta.gouv.fr' } - let(:subject) { 'Bonjour' } - let(:text) { "J'ai un problem" } - - it 'should call method' do - expect(api).to have_received(:create_conversation) - .with(email, subject, text, nil) - end - end - - context 'add_tags' do - before do - allow(api).to receive(:create_conversation) - .and_return( - double( - success?: true, - headers: { - 'Resource-ID' => conversation_id - } - ) - ) - - described_class.new(params, api).send_form - end - - let(:params) { - { - email: email, - subject: subject, - text: text, - tags: tags - } - } - let(:email) { 'paul.chavard@beta.gouv.fr' } - let(:subject) { 'Bonjour' } - let(:text) { "J'ai un problem" } - let(:tags) { ['info demarche'] } - let(:conversation_id) { '123' } - - it 'should call method' do - expect(api).to have_received(:create_conversation) - .with(email, subject, text, nil) - expect(api).to have_received(:add_tags) - .with(conversation_id, tags + ['contact form']) - end - end - - context 'add_phone' do - before do - allow(api).to receive(:create_conversation) - .and_return( - double( - success?: true, - headers: { - 'Resource-ID' => conversation_id - } - ) - ) - - described_class.new(params, api).send_form - end - - let(:params) { - { - email: email, - subject: subject, - text: text, - phone: '0666666666' - } - } - let(:phone) { '0666666666' } - let(:email) { 'paul.chavard@beta.gouv.fr' } - let(:subject) { 'Bonjour' } - let(:text) { "J'ai un problem" } - let(:tags) { ['info demarche'] } - let(:conversation_id) { '123' } - - it 'should call method' do - expect(api).to have_received(:create_conversation) - .with(email, subject, text, nil) - expect(api).to have_received(:add_phone_number) - .with(email, phone) - end - end - end -end From 4bc0a04106e07cdce3b2a80c831929945b0a55b1 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Tue, 30 Jul 2024 18:26:53 +0200 Subject: [PATCH 3/7] chore(schema): add contact_forms --- app/models/contact_form.rb | 2 ++ .../20240729160650_create_contact_forms.rb | 17 +++++++++++++++++ db/schema.rb | 18 +++++++++++++++++- spec/factories/contact_form.rb | 14 ++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 app/models/contact_form.rb create mode 100644 db/migrate/20240729160650_create_contact_forms.rb create mode 100644 spec/factories/contact_form.rb diff --git a/app/models/contact_form.rb b/app/models/contact_form.rb new file mode 100644 index 00000000000..d2a3495e1e2 --- /dev/null +++ b/app/models/contact_form.rb @@ -0,0 +1,2 @@ +class ContactForm < ApplicationRecord +end diff --git a/db/migrate/20240729160650_create_contact_forms.rb b/db/migrate/20240729160650_create_contact_forms.rb new file mode 100644 index 00000000000..7100760f7f4 --- /dev/null +++ b/db/migrate/20240729160650_create_contact_forms.rb @@ -0,0 +1,17 @@ +class CreateContactForms < ActiveRecord::Migration[7.0] + def change + create_table :contact_forms do |t| + t.string :email + t.string :subject, null: false + t.text :text, null: false + t.string :question_type, null: false + t.references :user, null: true, foreign_key: true + t.bigint :dossier_id # not a reference (dossier may not exist) + t.string :phone + t.string :tags, array: true, default: [] + t.boolean :for_admin, default: false, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0544b546ca6..49687353a56 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_07_16_091043) do +ActiveRecord::Schema[7.0].define(version: 2024_07_29_160650) do # These are extensions that must be enabled in order to support this database enable_extension "pg_buffercache" enable_extension "pg_stat_statements" @@ -315,6 +315,21 @@ t.index ["instructeur_id"], name: "index_commentaires_on_instructeur_id" end + create_table "contact_forms", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "dossier_id" + t.string "email" + t.boolean "for_admin", default: false, null: false + t.string "phone" + t.string "question_type", null: false + t.string "subject", null: false + t.string "tags", default: [], array: true + t.text "text", null: false + t.datetime "updated_at", null: false + t.bigint "user_id" + t.index ["user_id"], name: "index_contact_forms_on_user_id" + end + create_table "contact_informations", force: :cascade do |t| t.text "adresse", null: false t.datetime "created_at", null: false @@ -1229,6 +1244,7 @@ add_foreign_key "commentaires", "dossiers" add_foreign_key "commentaires", "experts" add_foreign_key "commentaires", "instructeurs" + add_foreign_key "contact_forms", "users" add_foreign_key "contact_informations", "groupe_instructeurs" add_foreign_key "dossier_assignments", "dossiers" add_foreign_key "dossier_batch_operations", "batch_operations" diff --git a/spec/factories/contact_form.rb b/spec/factories/contact_form.rb new file mode 100644 index 00000000000..a0da9e2bffc --- /dev/null +++ b/spec/factories/contact_form.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :contact_form do + user { nil } + email { 'test@example.com' } + dossier_id { nil } + subject { 'Test Subject' } + text { 'Test Content' } + question_type { 'lost_user' } + tags { ['test tag'] } + phone { nil } + end +end From 5af32b46f4ed9d43c3b3a456e419db2ab0e800b7 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Tue, 30 Jul 2024 19:14:04 +0200 Subject: [PATCH 4/7] refactor(contact): form is persisted in db before pushed to HS --- app/controllers/support_controller.rb | 57 ++++------ app/jobs/helpscout_create_conversation_job.rb | 36 +++--- app/lib/helpscout/form.rb | 78 ------------- app/models/contact_form.rb | 79 +++++++++++++ app/views/support/_form.html.haml | 20 ++-- app/views/support/index.html.haml | 67 +---------- config/locales/views/support/en.yml | 13 +-- config/locales/views/support/fr.yml | 13 +-- spec/controllers/support_controller_spec.rb | 106 ++++++++++++------ .../helpscout_create_conversation_job_spec.rb | 57 +++++----- 10 files changed, 236 insertions(+), 290 deletions(-) delete mode 100644 app/lib/helpscout/form.rb diff --git a/app/controllers/support_controller.rb b/app/controllers/support_controller.rb index f967f6fa25e..ff4dd5e95f5 100644 --- a/app/controllers/support_controller.rb +++ b/app/controllers/support_controller.rb @@ -2,25 +2,31 @@ class SupportController < ApplicationController invisible_captcha only: [:create], on_spam: :redirect_to_root def index - @form = Helpscout::Form.new(tags: tags_from_query_params, dossier_id: dossier&.id, current_user:) + @form = ContactForm.new(tags: support_form_params.fetch(:tags, []), dossier_id: dossier&.id) + @form.user = current_user end def admin - @form = Helpscout::Form.new(tags: tags_from_query_params, current_user:, for_admin: true) + @form = ContactForm.new(tags: support_form_params.fetch(:tags, []), for_admin: true) + @form.user = current_user end def create - if direct_message? && create_commentaire + if direct_message? + create_commentaire! flash.notice = "Votre message a été envoyé sur la messagerie de votre dossier." redirect_to messagerie_dossier_path(dossier) return end - @form = Helpscout::Form.new(support_form_params.except(:piece_jointe).merge(current_user:)) + form_params = support_form_params + @form = ContactForm.new(form_params.except(:piece_jointe)) + @form.piece_jointe.attach(form_params[:piece_jointe]) if form_params[:piece_jointe].present? + @form.user = current_user - if @form.valid? - create_conversation_later(@form) + if @form.save + @form.create_conversation_later flash.notice = "Votre message a été envoyé." redirect_to root_path @@ -32,29 +38,7 @@ def create private - def create_conversation_later(form) - if support_form_params[:piece_jointe].present? - blob = ActiveStorage::Blob.create_and_upload!( - io: support_form_params[:piece_jointe].tempfile, - filename: support_form_params[:piece_jointe].original_filename, - content_type: support_form_params[:piece_jointe].content_type, - identify: false - ).tap(&:scan_for_virus_later) - end - - HelpscoutCreateConversationJob.perform_later( - blob_id: blob&.id, - subject: form.subject, - email: current_user&.email || form.email, - phone: form.phone, - text: form.text, - dossier_id: form.dossier_id, - browser: browser_name, - tags: form.tags_array - ) - end - - def create_commentaire + def create_commentaire! attributes = { piece_jointe: support_form_params[:piece_jointe], body: "[#{support_form_params[:subject]}]

    #{support_form_params[:text]}" @@ -68,12 +52,11 @@ def browser_name end end - def tags_from_query_params - support_form_params[:tags]&.join(",") || "" - end - def direct_message? - user_signed_in? && support_form_params[:type] == Helpscout::Form::TYPE_INSTRUCTION && dossier.present? && dossier.messagerie_available? + return false unless user_signed_in? + return false unless support_form_params[:question_type] == ContactForm::TYPE_INSTRUCTION + + dossier&.messagerie_available? end def dossier @@ -85,9 +68,9 @@ def redirect_to_root end def support_form_params - keys = [:email, :subject, :text, :type, :dossier_id, :piece_jointe, :phone, :tags, :for_admin] - if params.key?(:helpscout_form) # submitting form - params.require(:helpscout_form).permit(*keys) + keys = [:email, :subject, :text, :question_type, :dossier_id, :piece_jointe, :phone, :for_admin, tags: []] + if params.key?(:contact_form) # submitting form + params.require(:contact_form).permit(*keys) else params.permit(:dossier_id, tags: []) # prefilling form end diff --git a/app/jobs/helpscout_create_conversation_job.rb b/app/jobs/helpscout_create_conversation_job.rb index 068fe651130..f12cab839ab 100644 --- a/app/jobs/helpscout_create_conversation_job.rb +++ b/app/jobs/helpscout_create_conversation_job.rb @@ -8,41 +8,49 @@ class FileNotScannedYetError < StandardError retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10 + attr_reader :contact_form attr_reader :api - def perform(blob_id: nil, **params) - if blob_id.present? - blob = ActiveStorage::Blob.find(blob_id) - raise FileNotScannedYetError if blob.virus_scanner.pending? + def perform(contact_form) + @contact_form = contact_form - blob = nil unless blob.virus_scanner.safe? + if contact_form.piece_jointe.attached? + raise FileNotScannedYetError if contact_form.piece_jointe.virus_scanner.pending? end @api = Helpscout::API.new - create_conversation(params, blob) + create_conversation + + contact_form.destroy end private - def create_conversation(params, blob) + def create_conversation response = api.create_conversation( - params[:email], - params[:subject], - params[:text], - blob + contact_form.email, + contact_form.subject, + contact_form.text, + safe_blob ) if response.success? conversation_id = response.headers['Resource-ID'] - if params[:phone].present? - api.add_phone_number(params[:email], params[:phone]) + if contact_form.phone.present? + api.add_phone_number(contact_form.email, contact_form.phone) end - api.add_tags(conversation_id, params[:tags]) + api.add_tags(conversation_id, contact_form.tags) else fail "Error while creating conversation: #{response.response_code} '#{response.body}'" end end + + def safe_blob + return if !contact_form.piece_jointe.virus_scanner&.safe? + + contact_form.piece_jointe + end end diff --git a/app/lib/helpscout/form.rb b/app/lib/helpscout/form.rb deleted file mode 100644 index 647c4e2a170..00000000000 --- a/app/lib/helpscout/form.rb +++ /dev/null @@ -1,78 +0,0 @@ -class Helpscout::Form - include ActiveModel::Model - include ActiveModel::Attributes - - attribute :email, :string - attribute :subject, :string - attribute :text, :string - attribute :type, :string - attribute :dossier_id, :integer - attribute :tags, :string - attribute :phone, :string - attribute :tags, :string - attribute :for_admin, :boolean, default: false - - validates :email, presence: true, strict_email: true, if: :require_email? # i18n-tasks-use t('activemodel.errors.models.helpscout/form.invalid_email_format') - validates :subject, presence: true - validates :text, presence: true - validates :type, presence: true - - attr_reader :current_user - attr_reader :options - - TYPE_INFO = 'procedure_info' - TYPE_PERDU = 'lost_user' - TYPE_INSTRUCTION = 'instruction_info' - TYPE_AMELIORATION = 'product' - TYPE_AUTRE = 'other' - - ADMIN_TYPE_RDV = 'admin_demande_rdv' - ADMIN_TYPE_QUESTION = 'admin_question' - ADMIN_TYPE_SOUCIS = 'admin_soucis' - ADMIN_TYPE_PRODUIT = 'admin_suggestion_produit' - ADMIN_TYPE_DEMANDE_COMPTE = 'admin_demande_compte' - ADMIN_TYPE_AUTRE = 'admin_autre' - - def self.default_options - [ - [I18n.t(:question, scope: [:support, :index, TYPE_INFO]), TYPE_INFO, I18n.t("links.common.faq.contacter_service_en_charge_url")], - [I18n.t(:question, scope: [:support, :index, TYPE_PERDU]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL], - [I18n.t(:question, scope: [:support, :index, TYPE_INSTRUCTION]), TYPE_INSTRUCTION, I18n.t("links.common.faq.ou_en_est_mon_dossier_url")], - [I18n.t(:question, scope: [:support, :index, TYPE_AMELIORATION]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL], - [I18n.t(:question, scope: [:support, :index, TYPE_AUTRE]), TYPE_AUTRE] - ] - end - - def self.admin_options - [ - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_QUESTION], app_name: Current.application_name), ADMIN_TYPE_QUESTION], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_RDV], app_name: Current.application_name), ADMIN_TYPE_RDV], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_SOUCIS], app_name: Current.application_name), ADMIN_TYPE_SOUCIS], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_PRODUIT]), ADMIN_TYPE_PRODUIT], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_DEMANDE_COMPTE]), ADMIN_TYPE_DEMANDE_COMPTE], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_AUTRE]), ADMIN_TYPE_AUTRE] - ] - end - - def initialize(params) - @current_user = params.delete(:current_user) - params[:email] = EmailSanitizableConcern::EmailSanitizer.sanitize(params[:email]) if params[:email].present? - super(params) - - @options = if for_admin? - self.class.admin_options - else - self.class.default_options - end - end - - alias for_admin? for_admin - - def tags_array - (tags&.split(",") || []) + ['contact form', type] - end - - def require_email? = current_user.blank? - - def persisted? = false -end diff --git a/app/models/contact_form.rb b/app/models/contact_form.rb index d2a3495e1e2..c1363e0bc73 100644 --- a/app/models/contact_form.rb +++ b/app/models/contact_form.rb @@ -1,2 +1,81 @@ class ContactForm < ApplicationRecord + attr_reader :options + + belongs_to :user, optional: true, dependent: :destroy + + after_initialize :set_options + before_validation :normalize_strings + before_validation :sanitize_email + before_save :add_default_tags + + validates :email, presence: true, strict_email: true, if: :require_email? + validates :subject, presence: true + validates :text, presence: true + validates :question_type, presence: true + + has_one_attached :piece_jointe + + TYPE_INFO = 'procedure_info' + TYPE_PERDU = 'lost_user' + TYPE_INSTRUCTION = 'instruction_info' + TYPE_AMELIORATION = 'product' + TYPE_AUTRE = 'other' + + ADMIN_TYPE_RDV = 'admin_demande_rdv' + ADMIN_TYPE_QUESTION = 'admin_question' + ADMIN_TYPE_SOUCIS = 'admin_soucis' + ADMIN_TYPE_PRODUIT = 'admin_suggestion_produit' + ADMIN_TYPE_DEMANDE_COMPTE = 'admin_demande_compte' + ADMIN_TYPE_AUTRE = 'admin_autre' + + def self.default_options + [ + [I18n.t(:question, scope: [:support, :index, TYPE_INFO]), TYPE_INFO, I18n.t("links.common.faq.contacter_service_en_charge_url")], + [I18n.t(:question, scope: [:support, :index, TYPE_PERDU]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL], + [I18n.t(:question, scope: [:support, :index, TYPE_INSTRUCTION]), TYPE_INSTRUCTION, I18n.t("links.common.faq.ou_en_est_mon_dossier_url")], + [I18n.t(:question, scope: [:support, :index, TYPE_AMELIORATION]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL], + [I18n.t(:question, scope: [:support, :index, TYPE_AUTRE]), TYPE_AUTRE] + ] + end + + def self.admin_options + [ + [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_QUESTION], app_name: Current.application_name), ADMIN_TYPE_QUESTION], + [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_RDV], app_name: Current.application_name), ADMIN_TYPE_RDV], + [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_SOUCIS], app_name: Current.application_name), ADMIN_TYPE_SOUCIS], + [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_PRODUIT]), ADMIN_TYPE_PRODUIT], + [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_DEMANDE_COMPTE]), ADMIN_TYPE_DEMANDE_COMPTE], + [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_AUTRE]), ADMIN_TYPE_AUTRE] + ] + end + + def for_admin=(value) + super(value) + set_options + end + + def create_conversation_later + HelpscoutCreateConversationJob.perform_later(self) + end + + def require_email? = user.blank? + + private + + def normalize_strings + self.subject = subject&.strip + self.text = text&.strip + end + + def sanitize_email + self.email = EmailSanitizableConcern::EmailSanitizer.sanitize(email) if email.present? + end + + def add_default_tags + self.tags = tags.push('contact form', question_type).uniq + end + + def set_options + @options = for_admin? ? self.class.admin_options : self.class.default_options + end end diff --git a/app/views/support/_form.html.haml b/app/views/support/_form.html.haml index bbe8747fea7..6e8cf241144 100644 --- a/app/views/support/_form.html.haml +++ b/app/views/support/_form.html.haml @@ -3,23 +3,23 @@ - if form.require_email? = render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email' }) do |c| - - c.with_label { Helpscout::Form.human_attribute_name(form.for_admin? ? :email_pro : :email) } + - c.with_label { ContactForm.human_attribute_name(form.for_admin? ? :email_pro : :email) } - %fieldset.fr-fieldset{ name: "type" } + %fieldset.fr-fieldset{ name: "question_type" } %legend.fr-fieldset__legend.fr-fieldset__legend--regular = t('.your_question') = render EditableChamp::AsteriskMandatoryComponent.new .fr-fieldset__content - form.options.each do |(question, question_type, link)| .fr-radio-group - = f.radio_button :type, question_type, required: true, data: {"support-target": "inputRadio" }, checked: question_type == form.type - = f.label "type_#{question_type}", { 'aria-controls': link ? "card-#{question_type}" : nil, class: 'fr-label' } do + = f.radio_button :question_type, question_type, required: true, data: {"support-target": "inputRadio" }, checked: question_type == form.question_type + = f.label "question_type_#{question_type}", { 'aria-controls': link ? "card-#{question_type}" : nil, class: 'fr-label' } do = question - if link.present? .fr-ml-3w{ id: "card-#{question_type}", - class: class_names('hidden' => question_type != form.type), - "aria-hidden": question_type != form.type, + class: class_names('hidden' => question_type != form.question_type), + "aria-hidden": question_type != form.question_type, data: { "support-target": "content" } } = render Dsfr::CalloutComponent.new(title: t('.our_answer')) do |c| - c.with_html_body do @@ -43,13 +43,15 @@ %span.fr-hint-text = t('.notice_upload_group') - %p.notice.hidden{ data: { 'contact-type-only': Helpscout::Form::TYPE_AMELIORATION } } + %p.notice.hidden{ data: { 'contact-type-only': ContactForm::TYPE_AMELIORATION } } = t('.notice_pj_product') - %p.notice.hidden{ data: { 'contact-type-only': Helpscout::Form::TYPE_AUTRE } } + %p.notice.hidden{ data: { 'contact-type-only': ContactForm::TYPE_AUTRE } } = t('.notice_pj_other') = f.file_field :piece_jointe, class: 'fr-upload', accept: '.jpg, .jpeg, .png, .pdf' - = f.hidden_field :tags + - f.object.tags.each_with_index do |tag, index| + = f.hidden_field :tags, name: f.field_name(:tags, multiple: true), id: f.field_id(:tag, index), value: tag + = f.hidden_field :for_admin = invisible_captcha diff --git a/app/views/support/index.html.haml b/app/views/support/index.html.haml index 237e5fa86b6..0a33c87f180 100644 --- a/app/views/support/index.html.haml +++ b/app/views/support/index.html.haml @@ -9,69 +9,4 @@ .fr-highlight= t('.intro_html') - .description - .recommandations - = t('.intro_html') - %p.mandatory-explanation= t('asterisk_html', scope: [:utils]) - - - if !user_signed_in? - .fr-input-group - = label_tag :email, class: 'fr-label' do - Email - = render EditableChamp::AsteriskMandatoryComponent.new - %span.fr-hint-text - = t('.notice_email') - = email_field_tag :email, params[:email], required: true, autocomplete: 'email', class: 'fr-input' - - %fieldset.fr-fieldset{ name: "type" } - %legend.fr-fieldset__legend - = t('.your_question') - = render EditableChamp::AsteriskMandatoryComponent.new - .fr-fieldset__content - - @options.each do |(question, question_type, link)| - .fr-radio-group - = radio_button_tag :type, question_type, false, required: true, data: {"support-target": "inputRadio" } - = label_tag "type_#{question_type}", { 'aria-controls': link ? "card-#{question_type}" : nil, class: 'fr-label' } do - = question - - - if link.present? - .fr-ml-3w.hidden{ id: "card-#{question_type}", "aria-hidden": true , data: { "support-target": "content" } } - = render Dsfr::CalloutComponent.new(title: t('.our_answer')) do |c| - - c.with_html_body do - -# i18n-tasks-use t("support.index.#{question_type}.answer_html") - = t('answer_html', scope: [:support, :index, question_type], base_url: Current.application_base_url, "link_#{question_type}": link) - - .fr-input-group - = label_tag :dossier_id, t('file_number', scope: [:utils]), class: 'fr-label' - = text_field_tag :dossier_id, @dossier_id, class: 'fr-input' - - .fr-input-group - = label_tag :subject, class: 'fr-label' do - = t('subject', scope: [:utils]) - = render EditableChamp::AsteriskMandatoryComponent.new - = text_field_tag :subject, params[:subject], required: true, class: 'fr-input' - - .fr-input-group - = label_tag :text, class: 'fr-label' do - = t('message', scope: [:utils]) - = render EditableChamp::AsteriskMandatoryComponent.new - = text_area_tag :text, params[:text], rows: 6, required: true, class: 'fr-input' - - .fr-upload-group - = label_tag :piece_jointe, class: 'fr-label' do - = t('pj', scope: [:utils]) - %span.fr-hint-text - = t('.notice_upload_group') - - %p.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AMELIORATION } } - = t('.notice_pj_product') - %p.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AUTRE } } - = t('.notice_pj_other') - = file_field_tag :piece_jointe, class: 'fr-upload', accept: '.jpg, .jpeg, .png, .pdf' - - = hidden_field_tag :tags, @tags&.join(',') - - = invisible_captcha - - .send-wrapper.fr-my-3w - = button_tag t('send_mail', scope: [:utils]), type: :submit, class: 'fr-btn send' + = render partial: "form", object: @form diff --git a/config/locales/views/support/en.yml b/config/locales/views/support/en.yml index 6378df37e29..69852a6ecc3 100644 --- a/config/locales/views/support/en.yml +++ b/config/locales/views/support/en.yml @@ -1,7 +1,7 @@ en: - activemodel: + activerecord: attributes: - helpscout/form: + contact_form: email: 'Your email address' email_pro: Professional email address phone: Professional phone number (direct line) @@ -12,15 +12,10 @@ en: email: 'Example: address@mail.com' errors: models: - helpscout/form: + contact_form: invalid_email_format: 'is not valid' support: - index: - contact: Contact - intro_html: - '

    Contact us via this form and we will answer you as quickly as possible.

    -

    Make sure you provide all the required information so we can help you in the best way.

    ' - notice_email: 'Expected format: address@mail.com' + form: your_question: Your question our_answer: Our answer notice_pj_product: A screenshot can help us identify the element to improve. diff --git a/config/locales/views/support/fr.yml b/config/locales/views/support/fr.yml index eb6f74a5808..e3ab7a913cd 100644 --- a/config/locales/views/support/fr.yml +++ b/config/locales/views/support/fr.yml @@ -1,7 +1,7 @@ fr: - activemodel: + activerecord: attributes: - helpscout/form: + contact_form: email: 'Votre adresse email' email_pro: Votre adresse email professionnelle phone: Numéro de téléphone professionnel (ligne directe) @@ -12,15 +12,10 @@ fr: email: 'Exemple: adresse@mail.com' errors: models: - helpscout/form: + contact_form: invalid_email_format: 'est invalide' support: - index: - contact: Contact - intro_html: - '

    Contactez-nous via ce formulaire et nous vous répondrons dans les plus brefs délais.

    -

    Pensez bien à nous donner le plus d’informations possible pour que nous puissions vous aider au mieux.

    ' - notice_email: 'Format attendu : adresse@mail.com' + form: your_question: Votre question our_answer: Notre réponse notice_pj_product: Une capture d’écran peut nous aider à identifier plus facilement l’endroit à améliorer. diff --git a/spec/controllers/support_controller_spec.rb b/spec/controllers/support_controller_spec.rb index 55915b0f124..4e19ffb09a0 100644 --- a/spec/controllers/support_controller_spec.rb +++ b/spec/controllers/support_controller_spec.rb @@ -1,4 +1,4 @@ -describe SupportController, type: :controller do +describe SupportController, question_type: :controller do render_views context 'signed in' do @@ -45,22 +45,30 @@ get :index, params: { tags: tags } expect(response.status).to eq(200) - expect(response.body).to include(tags.join(',')) + expect(response.body).to include("value=\"yolo\"") + expect(response.body).to include("value=\"toto\"") end end describe "send form" do subject do - post :create, params: { helpscout_form: params } + post :create, params: { contact_form: params } end context "when invisible captcha is ignored" do - let(:params) { { subject: 'bonjour', text: 'un message', type: 'procedure_info' } } + let(:params) { { subject: 'bonjour', text: 'un message', question_type: 'procedure_info' } } it 'creates a conversation on HelpScout' do expect { subject }.to \ change(Commentaire, :count).by(0).and \ - have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:type))) + change(ContactForm, :count).by(1) + + contact_form = ContactForm.last + expect(HelpscoutCreateConversationJob).to have_been_enqueued.with(contact_form) + + expect(contact_form.subject).to eq("bonjour") + expect(contact_form.text).to eq("un message") + expect(contact_form.tags).to include("procedure_info") expect(flash[:notice]).to match('Votre message a été envoyé.') expect(response).to redirect_to root_path @@ -73,7 +81,7 @@ let(:params) do { dossier_id: dossier.id, - type: Helpscout::Form::TYPE_INSTRUCTION, + question_type: ContactForm::TYPE_INSTRUCTION, subject: 'bonjour', text: 'un message' } @@ -82,7 +90,11 @@ it 'creates a conversation on HelpScout' do expect { subject }.to \ change(Commentaire, :count).by(0).and \ - have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(subject: 'bonjour', dossier_id: dossier.id)) + change(ContactForm, :count).by(1) + + contact_form = ContactForm.last + expect(HelpscoutCreateConversationJob).to have_been_enqueued.with(contact_form) + expect(contact_form.dossier_id).to eq(dossier.id) expect(flash[:notice]).to match('Votre message a été envoyé.') expect(response).to redirect_to root_path @@ -96,7 +108,7 @@ let(:params) do { dossier_id: dossier.id, - type: Helpscout::Form::TYPE_INSTRUCTION, + question_type: ContactForm::TYPE_INSTRUCTION, subject: 'bonjour', text: 'un message' } @@ -120,7 +132,7 @@ context "when invisible captcha is filled" do subject do post :create, params: { - helpscout_form: { subject: 'bonjour', text: 'un message', type: 'procedure_info' }, + contact_form: { subject: 'bonjour', text: 'un message', question_type: 'procedure_info' }, InvisibleCaptcha.honeypots.sample => 'boom' } end @@ -156,15 +168,19 @@ describe 'send form' do subject do - post :create, params: { helpscout_form: params } + post :create, params: { contact_form: params } end - let(:params) { { subject: 'bonjour', email: "me@rspec.net", text: 'un message', type: 'procedure_info' } } + let(:params) { { subject: 'bonjour', email: "me@rspec.net", text: 'un message', question_type: 'procedure_info' } } it 'creates a conversation on HelpScout' do expect { subject }.to \ change(Commentaire, :count).by(0).and \ - have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:type))) + change(ContactForm, :count).by(1) + + contact_form = ContactForm.last + expect(HelpscoutCreateConversationJob).to have_been_enqueued.with(contact_form) + expect(contact_form.email).to eq("me@rspec.net") expect(flash[:notice]).to match('Votre message a été envoyé.') expect(response).to redirect_to root_path @@ -184,39 +200,57 @@ end context 'contact admin' do - subject do - post :create, params: { helpscout_form: params } + context 'index' do + it 'should have professionnal email field' do + get :admin + expect(response.body).to have_text('Votre adresse email professionnelle') + expect(response.body).to have_text('téléphone') + expect(response.body).to include('for_admin') + end end - let(:params) { { for_admin: "true", email: "email@pro.fr", subject: 'bonjour', text: 'un message', type: 'admin question', phone: '06' } } - - describe "when form is filled" do - it "creates a conversation on HelpScout" do - expect { subject }.to have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params.except(:for_admin, :type))) - expect(flash[:notice]).to match('Votre message a été envoyé.') + context 'create' do + subject do + post :create, params: { contact_form: params } end - context "with a piece justificative" do - let(:logo) { fixture_file_upload('spec/fixtures/files/white.png', 'image/png') } - let(:params) { super().merge(piece_jointe: logo) } + let(:params) { { for_admin: "true", email: "email@pro.fr", subject: 'bonjour', text: 'un message', question_type: 'admin question', phone: '06' } } - it "create blob and pass it to conversation job" do - expect { subject }.to \ - change(ActiveStorage::Blob, :count).by(1).and \ - have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(blob_id: Integer)).and \ - have_enqueued_job(VirusScannerJob) + describe "when form is filled" do + it "creates a conversation on HelpScout" do + expect { subject }.to change(ContactForm, :count).by(1) + + contact_form = ContactForm.last + expect(HelpscoutCreateConversationJob).to have_been_enqueued.with(contact_form) + expect(contact_form.email).to eq(params[:email]) + expect(contact_form.phone).to eq("06") + expect(contact_form.tags).to match_array(["admin question", "contact form"]) + + expect(flash[:notice]).to match('Votre message a été envoyé.') end - end - end - describe "when invisible captcha is filled" do - subject do - post :create, params: { helpscout_form: params, InvisibleCaptcha.honeypots.sample => 'boom' } + context "with a piece justificative" do + let(:logo) { fixture_file_upload('spec/fixtures/files/white.png', 'image/png') } + let(:params) { super().merge(piece_jointe: logo) } + + it "create blob and pass it to conversation job" do + expect { subject }.to change(ContactForm, :count).by(1) + + contact_form = ContactForm.last + expect(contact_form.piece_jointe).to be_attached + end + end end - it 'does not create a conversation on HelpScout' do - subject - expect(flash[:alert]).to eq(I18n.t('invisible_captcha.sentence_for_humans')) + describe "when invisible captcha is filled" do + subject do + post :create, params: { contact_form: params, InvisibleCaptcha.honeypots.sample => 'boom' } + end + + it 'does not create a conversation on HelpScout' do + subject + expect(flash[:alert]).to eq(I18n.t('invisible_captcha.sentence_for_humans')) + end end end end diff --git a/spec/jobs/helpscout_create_conversation_job_spec.rb b/spec/jobs/helpscout_create_conversation_job_spec.rb index 1aa36a5d467..5b45084f4d9 100644 --- a/spec/jobs/helpscout_create_conversation_job_spec.rb +++ b/spec/jobs/helpscout_create_conversation_job_spec.rb @@ -6,16 +6,9 @@ let(:subject_text) { 'Bonjour' } let(:text) { "J'ai un pb" } let(:tags) { ["first tag"] } + let(:question_type) { "lost" } let(:phone) { nil } - let(:params) { - { - email:, - subject: subject_text, - text:, - tags:, - phone: - } - } + let(:contact_form) { create(:contact_form, email:, subject: subject_text, text:, tags:, phone:, question_type:) } describe '#perform' do before do @@ -26,56 +19,55 @@ headers: { 'Resource-ID' => 'new-conversation-id' } )) allow(api).to receive(:add_tags) - allow(api).to receive(:add_phone_number) if params[:phone].present? + allow(api).to receive(:add_phone_number) if phone.present? end subject { - described_class.perform_now(**params) + described_class.perform_now(contact_form) } - context 'when blob_id is not present' do + context 'when no file is attached' do it 'sends the form without a file' do subject expect(api).to have_received(:create_conversation).with(email, subject_text, text, nil) - expect(api).to have_received(:add_tags).with("new-conversation-id", tags) + expect(api).to have_received(:add_tags).with("new-conversation-id", match_array(tags.concat(["contact form", question_type]))) + expect(contact_form).to be_destroyed end end - context 'when blob_id is present' do - let(:blob) { - ActiveStorage::Blob.create_and_upload!(io: StringIO.new("toto"), filename: "toto.png") - } - let(:params) { super().merge(blob_id: blob.id) } - + context 'when a file is attached' do before do - allow(blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: pending, safe?: safe)) - allow(ActiveStorage::Blob).to receive(:find).with(blob.id).and_return(blob) + file = fixture_file_upload('spec/fixtures/files/white.png', 'image/png') + contact_form.piece_jointe.attach(file) end context 'when the file has not been scanned yet' do - let(:pending) { true } - let(:safe) { false } + before do + allow_any_instance_of(ActiveStorage::Blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: true, safe?: false)) + end - it 'reenqueue job' do - expect { subject }.to have_enqueued_job(described_class).with(params) + it 'reenqueues job' do + expect { subject }.to have_enqueued_job(described_class).with(contact_form) end end context 'when the file is safe' do - let(:pending) { false } - let(:safe) { true } + before do + allow_any_instance_of(ActiveStorage::Blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: false, safe?: true)) + end - it 'downloads the file and sends the form' do + it 'sends the form with the file' do subject - expect(api).to have_received(:create_conversation).with(email, subject_text, text, blob) + expect(api).to have_received(:create_conversation).with(email, subject_text, text, contact_form.piece_jointe) end end context 'when the file is not safe' do - let(:pending) { false } - let(:safe) { false } + before do + allow_any_instance_of(ActiveStorage::Blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: false, safe?: false)) + end - it 'ignore the file' do + it 'ignores the file' do subject expect(api).to have_received(:create_conversation).with(email, subject_text, text, nil) end @@ -84,6 +76,7 @@ context 'with a phone' do let(:phone) { "06" } + it 'associates the phone number' do subject expect(api).to have_received(:add_phone_number).with(email, phone) From e71c1781a91bf38ed6b97131f38906a41831e099 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 31 Jul 2024 11:59:50 +0200 Subject: [PATCH 5/7] fix(contact): add missing notices locales --- app/controllers/support_controller.rb | 4 ++-- config/locales/views/support/en.yml | 3 +++ config/locales/views/support/fr.yml | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/controllers/support_controller.rb b/app/controllers/support_controller.rb index ff4dd5e95f5..f7622722617 100644 --- a/app/controllers/support_controller.rb +++ b/app/controllers/support_controller.rb @@ -14,7 +14,7 @@ def admin def create if direct_message? create_commentaire! - flash.notice = "Votre message a été envoyé sur la messagerie de votre dossier." + flash.notice = t('.direct_message_sent') redirect_to messagerie_dossier_path(dossier) return @@ -27,7 +27,7 @@ def create if @form.save @form.create_conversation_later - flash.notice = "Votre message a été envoyé." + flash.notice = t('.message_sent') redirect_to root_path else diff --git a/config/locales/views/support/en.yml b/config/locales/views/support/en.yml index 69852a6ecc3..ca545026a6b 100644 --- a/config/locales/views/support/en.yml +++ b/config/locales/views/support/en.yml @@ -68,3 +68,6 @@ en: question: I want to open an admin account with an Orange, Wanadoo, etc. email admin_autre: question: Other topic + create: + direct_message_sent: Your message has been sent to the mailbox in your file. + message_sent: Your message has been sent. diff --git a/config/locales/views/support/fr.yml b/config/locales/views/support/fr.yml index e3ab7a913cd..9ceaa6df660 100644 --- a/config/locales/views/support/fr.yml +++ b/config/locales/views/support/fr.yml @@ -69,3 +69,6 @@ fr: question: Je souhaite ouvrir un compte administrateur avec un email Orange, Wanadoo, etc. admin_autre: question: Autre sujet + create: + direct_message_sent: Votre message a été envoyé sur la messagerie de votre dossier. + message_sent: Votre message a été envoyé. From 4b6d3ee16d0fdea3baa8f71475beb323cc6aeb78 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 31 Jul 2024 16:20:49 +0200 Subject: [PATCH 6/7] refactor(contact): support => contact --- ...rt_controller.rb => contact_controller.rb} | 18 +++++++-------- ...rt_controller.ts => contact_controller.ts} | 2 +- app/models/contact_form.rb | 22 +++++++++---------- .../{support => contact}/_form.html.haml | 10 ++++----- .../{support => contact}/admin.html.haml | 0 .../{support => contact}/index.html.haml | 0 .../locales/views/{support => contact}/en.yml | 2 +- .../locales/views/{support => contact}/fr.yml | 2 +- config/routes.rb | 6 ++--- ...ler_spec.rb => contact_controller_spec.rb} | 2 +- 10 files changed, 32 insertions(+), 32 deletions(-) rename app/controllers/{support_controller.rb => contact_controller.rb} (77%) rename app/javascript/controllers/{support_controller.ts => contact_controller.ts} (95%) rename app/views/{support => contact}/_form.html.haml (89%) rename app/views/{support => contact}/admin.html.haml (100%) rename app/views/{support => contact}/index.html.haml (100%) rename config/locales/views/{support => contact}/en.yml (99%) rename config/locales/views/{support => contact}/fr.yml (99%) rename spec/controllers/{support_controller_spec.rb => contact_controller_spec.rb} (99%) diff --git a/app/controllers/support_controller.rb b/app/controllers/contact_controller.rb similarity index 77% rename from app/controllers/support_controller.rb rename to app/controllers/contact_controller.rb index f7622722617..ec2805df1a9 100644 --- a/app/controllers/support_controller.rb +++ b/app/controllers/contact_controller.rb @@ -1,13 +1,13 @@ -class SupportController < ApplicationController +class ContactController < ApplicationController invisible_captcha only: [:create], on_spam: :redirect_to_root def index - @form = ContactForm.new(tags: support_form_params.fetch(:tags, []), dossier_id: dossier&.id) + @form = ContactForm.new(tags: contact_form_params.fetch(:tags, []), dossier_id: dossier&.id) @form.user = current_user end def admin - @form = ContactForm.new(tags: support_form_params.fetch(:tags, []), for_admin: true) + @form = ContactForm.new(tags: contact_form_params.fetch(:tags, []), for_admin: true) @form.user = current_user end @@ -20,7 +20,7 @@ def create return end - form_params = support_form_params + form_params = contact_form_params @form = ContactForm.new(form_params.except(:piece_jointe)) @form.piece_jointe.attach(form_params[:piece_jointe]) if form_params[:piece_jointe].present? @form.user = current_user @@ -40,8 +40,8 @@ def create def create_commentaire! attributes = { - piece_jointe: support_form_params[:piece_jointe], - body: "[#{support_form_params[:subject]}]

    #{support_form_params[:text]}" + piece_jointe: contact_form_params[:piece_jointe], + body: "[#{contact_form_params[:subject]}]

    #{contact_form_params[:text]}" } CommentaireService.create!(current_user, dossier, attributes) end @@ -54,20 +54,20 @@ def browser_name def direct_message? return false unless user_signed_in? - return false unless support_form_params[:question_type] == ContactForm::TYPE_INSTRUCTION + return false unless contact_form_params[:question_type] == ContactForm::TYPE_INSTRUCTION dossier&.messagerie_available? end def dossier - @dossier ||= current_user&.dossiers&.find_by(id: support_form_params[:dossier_id]) + @dossier ||= current_user&.dossiers&.find_by(id: contact_form_params[:dossier_id]) end def redirect_to_root redirect_to root_path, alert: t('invisible_captcha.sentence_for_humans') end - def support_form_params + def contact_form_params keys = [:email, :subject, :text, :question_type, :dossier_id, :piece_jointe, :phone, :for_admin, tags: []] if params.key?(:contact_form) # submitting form params.require(:contact_form).permit(*keys) diff --git a/app/javascript/controllers/support_controller.ts b/app/javascript/controllers/contact_controller.ts similarity index 95% rename from app/javascript/controllers/support_controller.ts rename to app/javascript/controllers/contact_controller.ts index 22d947f0e37..436adc913a1 100644 --- a/app/javascript/controllers/support_controller.ts +++ b/app/javascript/controllers/contact_controller.ts @@ -1,7 +1,7 @@ import { ApplicationController } from './application_controller'; import { hide, show } from '@utils'; -export class SupportController extends ApplicationController { +export class ContactController extends ApplicationController { static targets = ['inputRadio', 'content']; declare readonly inputRadioTargets: HTMLInputElement[]; diff --git a/app/models/contact_form.rb b/app/models/contact_form.rb index c1363e0bc73..4e518e59891 100644 --- a/app/models/contact_form.rb +++ b/app/models/contact_form.rb @@ -30,22 +30,22 @@ class ContactForm < ApplicationRecord def self.default_options [ - [I18n.t(:question, scope: [:support, :index, TYPE_INFO]), TYPE_INFO, I18n.t("links.common.faq.contacter_service_en_charge_url")], - [I18n.t(:question, scope: [:support, :index, TYPE_PERDU]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL], - [I18n.t(:question, scope: [:support, :index, TYPE_INSTRUCTION]), TYPE_INSTRUCTION, I18n.t("links.common.faq.ou_en_est_mon_dossier_url")], - [I18n.t(:question, scope: [:support, :index, TYPE_AMELIORATION]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL], - [I18n.t(:question, scope: [:support, :index, TYPE_AUTRE]), TYPE_AUTRE] + [I18n.t(:question, scope: [:contact, :index, TYPE_INFO]), TYPE_INFO, I18n.t("links.common.faq.contacter_service_en_charge_url")], + [I18n.t(:question, scope: [:contact, :index, TYPE_PERDU]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL], + [I18n.t(:question, scope: [:contact, :index, TYPE_INSTRUCTION]), TYPE_INSTRUCTION, I18n.t("links.common.faq.ou_en_est_mon_dossier_url")], + [I18n.t(:question, scope: [:contact, :index, TYPE_AMELIORATION]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL], + [I18n.t(:question, scope: [:contact, :index, TYPE_AUTRE]), TYPE_AUTRE] ] end def self.admin_options [ - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_QUESTION], app_name: Current.application_name), ADMIN_TYPE_QUESTION], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_RDV], app_name: Current.application_name), ADMIN_TYPE_RDV], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_SOUCIS], app_name: Current.application_name), ADMIN_TYPE_SOUCIS], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_PRODUIT]), ADMIN_TYPE_PRODUIT], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_DEMANDE_COMPTE]), ADMIN_TYPE_DEMANDE_COMPTE], - [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_AUTRE]), ADMIN_TYPE_AUTRE] + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_QUESTION], app_name: Current.application_name), ADMIN_TYPE_QUESTION], + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_RDV], app_name: Current.application_name), ADMIN_TYPE_RDV], + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_SOUCIS], app_name: Current.application_name), ADMIN_TYPE_SOUCIS], + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_PRODUIT]), ADMIN_TYPE_PRODUIT], + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_DEMANDE_COMPTE]), ADMIN_TYPE_DEMANDE_COMPTE], + [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_AUTRE]), ADMIN_TYPE_AUTRE] ] end diff --git a/app/views/support/_form.html.haml b/app/views/contact/_form.html.haml similarity index 89% rename from app/views/support/_form.html.haml rename to app/views/contact/_form.html.haml index 6e8cf241144..c284df56682 100644 --- a/app/views/support/_form.html.haml +++ b/app/views/contact/_form.html.haml @@ -1,4 +1,4 @@ -= form_for form, url: contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :support } do |f| += form_for form, url: contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :contact } do |f| %p.fr-hint-text= t('asterisk_html', scope: [:utils]) - if form.require_email? @@ -12,7 +12,7 @@ .fr-fieldset__content - form.options.each do |(question, question_type, link)| .fr-radio-group - = f.radio_button :question_type, question_type, required: true, data: {"support-target": "inputRadio" }, checked: question_type == form.question_type + = f.radio_button :question_type, question_type, required: true, data: {"contact-target": "inputRadio" }, checked: question_type == form.question_type = f.label "question_type_#{question_type}", { 'aria-controls': link ? "card-#{question_type}" : nil, class: 'fr-label' } do = question @@ -20,11 +20,11 @@ .fr-ml-3w{ id: "card-#{question_type}", class: class_names('hidden' => question_type != form.question_type), "aria-hidden": question_type != form.question_type, - data: { "support-target": "content" } } + data: { "contact-target": "content" } } = render Dsfr::CalloutComponent.new(title: t('.our_answer')) do |c| - c.with_html_body do - -# i18n-tasks-use t("support.index.#{question_type}.answer_html") - = t('answer_html', scope: [:support, :index, question_type], base_url: Current.application_base_url, "link_#{question_type}": link) + -# i18n-tasks-use t("contact.index.#{question_type}.answer_html") + = t('answer_html', scope: [:contact, :index, question_type], base_url: Current.application_base_url, "link_#{question_type}": link) - if form.for_admin? diff --git a/app/views/support/admin.html.haml b/app/views/contact/admin.html.haml similarity index 100% rename from app/views/support/admin.html.haml rename to app/views/contact/admin.html.haml diff --git a/app/views/support/index.html.haml b/app/views/contact/index.html.haml similarity index 100% rename from app/views/support/index.html.haml rename to app/views/contact/index.html.haml diff --git a/config/locales/views/support/en.yml b/config/locales/views/contact/en.yml similarity index 99% rename from config/locales/views/support/en.yml rename to config/locales/views/contact/en.yml index ca545026a6b..c0815a28707 100644 --- a/config/locales/views/support/en.yml +++ b/config/locales/views/contact/en.yml @@ -14,7 +14,7 @@ en: models: contact_form: invalid_email_format: 'is not valid' - support: + contact: form: your_question: Your question our_answer: Our answer diff --git a/config/locales/views/support/fr.yml b/config/locales/views/contact/fr.yml similarity index 99% rename from config/locales/views/support/fr.yml rename to config/locales/views/contact/fr.yml index 9ceaa6df660..6d5b3d62991 100644 --- a/config/locales/views/support/fr.yml +++ b/config/locales/views/contact/fr.yml @@ -14,7 +14,7 @@ fr: models: contact_form: invalid_email_format: 'est invalide' - support: + contact: form: your_question: Votre question our_answer: Notre réponse diff --git a/config/routes.rb b/config/routes.rb index 9c223926774..cc1f9f7b37f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -224,10 +224,10 @@ get "suivi" => "root#suivi" post "save_locale" => "root#save_locale" - get "contact", to: "support#index" - post "contact", to: "support#create" + get "contact", to: "contact#index" + post "contact", to: "contact#create" - get "contact-admin", to: "support#admin" + get "contact-admin", to: "contact#admin" get "mentions-legales", to: "static_pages#legal_notice" get "declaration-accessibilite", to: "static_pages#accessibility_statement" diff --git a/spec/controllers/support_controller_spec.rb b/spec/controllers/contact_controller_spec.rb similarity index 99% rename from spec/controllers/support_controller_spec.rb rename to spec/controllers/contact_controller_spec.rb index 4e19ffb09a0..44bafc57de6 100644 --- a/spec/controllers/support_controller_spec.rb +++ b/spec/controllers/contact_controller_spec.rb @@ -1,4 +1,4 @@ -describe SupportController, question_type: :controller do +describe ContactController, question_type: :controller do render_views context 'signed in' do From e415c79ade62b2474d566bf1aba23985242b7619 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 31 Jul 2024 16:29:45 +0200 Subject: [PATCH 7/7] refactor(contact): builtin piece jointe attachment --- app/controllers/contact_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/contact_controller.rb b/app/controllers/contact_controller.rb index ec2805df1a9..c037f47efee 100644 --- a/app/controllers/contact_controller.rb +++ b/app/controllers/contact_controller.rb @@ -20,9 +20,7 @@ def create return end - form_params = contact_form_params - @form = ContactForm.new(form_params.except(:piece_jointe)) - @form.piece_jointe.attach(form_params[:piece_jointe]) if form_params[:piece_jointe].present? + @form = ContactForm.new(contact_form_params) @form.user = current_user if @form.save