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
diff --git a/app/controllers/contact_controller.rb b/app/controllers/contact_controller.rb
new file mode 100644
index 00000000000..c037f47efee
--- /dev/null
+++ b/app/controllers/contact_controller.rb
@@ -0,0 +1,76 @@
+class ContactController < ApplicationController
+ invisible_captcha only: [:create], on_spam: :redirect_to_root
+
+ def index
+ @form = ContactForm.new(tags: contact_form_params.fetch(:tags, []), dossier_id: dossier&.id)
+ @form.user = current_user
+ end
+
+ def admin
+ @form = ContactForm.new(tags: contact_form_params.fetch(:tags, []), for_admin: true)
+ @form.user = current_user
+ end
+
+ def create
+ if direct_message?
+ create_commentaire!
+ flash.notice = t('.direct_message_sent')
+
+ redirect_to messagerie_dossier_path(dossier)
+ return
+ end
+
+ @form = ContactForm.new(contact_form_params)
+ @form.user = current_user
+
+ if @form.save
+ @form.create_conversation_later
+ flash.notice = t('.message_sent')
+
+ redirect_to root_path
+ else
+ flash.alert = @form.errors.full_messages
+ render @form.for_admin ? :admin : :index
+ end
+ end
+
+ private
+
+ def create_commentaire!
+ attributes = {
+ piece_jointe: contact_form_params[:piece_jointe],
+ body: "[#{contact_form_params[:subject]}]
#{contact_form_params[:text]}"
+ }
+ CommentaireService.create!(current_user, dossier, attributes)
+ end
+
+ def browser_name
+ if browser.known?
+ "#{browser.name} #{browser.version} (#{browser.platform.name})"
+ end
+ end
+
+ def direct_message?
+ return false unless user_signed_in?
+ return false unless contact_form_params[:question_type] == ContactForm::TYPE_INSTRUCTION
+
+ dossier&.messagerie_available?
+ end
+
+ def dossier
+ @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 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)
+ else
+ params.permit(:dossier_id, tags: []) # prefilling form
+ end
+ end
+end
diff --git a/app/controllers/support_controller.rb b/app/controllers/support_controller.rb
deleted file mode 100644
index e2e536499d8..00000000000
--- a/app/controllers/support_controller.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-class SupportController < ApplicationController
- invisible_captcha only: [:create], on_spam: :redirect_to_root
-
- def index
- setup_context
- end
-
- def admin
- setup_context_admin
- end
-
- def create
- 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
-
- create_conversation_later
- flash.notice = "Votre message a été envoyé."
-
- if params[:admin]
- redirect_to root_path(formulaire_contact_admin_submitted: true)
- else
- redirect_to root_path(formulaire_contact_general_submitted: true)
- 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]
- blob = ActiveStorage::Blob.create_and_upload!(
- io: params[:piece_jointe].tempfile,
- filename: params[:piece_jointe].original_filename,
- content_type: 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,
- browser: browser_name,
- tags: tags
- )
- end
-
- def create_commentaire
- attributes = {
- piece_jointe: params[:piece_jointe],
- body: "[#{params[:subject]}]
#{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?
- end
-
- def dossier
- @dossier ||= current_user&.dossiers&.find_by(id: params[:dossier_id])
- end
-
- def email
- current_user&.email || params[:email]
- end
-
- def redirect_to_root
- redirect_to root_path, alert: t('invisible_captcha.sentence_for_humans')
- 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/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/jobs/helpscout_create_conversation_job.rb b/app/jobs/helpscout_create_conversation_job.rb
index a8175d0294c..f12cab839ab 100644
--- a/app/jobs/helpscout_create_conversation_job.rb
+++ b/app/jobs/helpscout_create_conversation_job.rb
@@ -8,14 +8,49 @@ class FileNotScannedYetError < StandardError
retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10
- def perform(blob_id: nil, **args)
- if blob_id.present?
- blob = ActiveStorage::Blob.find(blob_id)
- raise FileNotScannedYetError if blob.virus_scanner.pending?
+ attr_reader :contact_form
+ attr_reader :api
- blob = nil unless blob.virus_scanner.safe?
+ def perform(contact_form)
+ @contact_form = contact_form
+
+ if contact_form.piece_jointe.attached?
+ raise FileNotScannedYetError if contact_form.piece_jointe.virus_scanner.pending?
end
- Helpscout::FormAdapter.new(**args, blob:).send_form
+ @api = Helpscout::API.new
+
+ create_conversation
+
+ contact_form.destroy
+ end
+
+ private
+
+ def create_conversation
+ response = api.create_conversation(
+ contact_form.email,
+ contact_form.subject,
+ contact_form.text,
+ safe_blob
+ )
+
+ if response.success?
+ conversation_id = response.headers['Resource-ID']
+
+ if contact_form.phone.present?
+ api.add_phone_number(contact_form.email, contact_form.phone)
+ end
+
+ 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_adapter.rb b/app/lib/helpscout/form_adapter.rb
deleted file mode 100644
index 4127e9dc252..00000000000
--- a/app/lib/helpscout/form_adapter.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-class Helpscout::FormAdapter
- attr_reader :params
-
- def self.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 = {}, 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 send_form
- conversation_id = create_conversation
-
- if conversation_id.present?
- add_tags(conversation_id)
- true
- else
- false
- end
- end
-
- private
-
- def add_tags(conversation_id)
- @api.add_tags(conversation_id, tags)
- end
-
- def tags
- (params[:tags].presence || []) + ['contact form']
- end
-
- def create_conversation
- response = @api.create_conversation(
- params[:email],
- params[:subject],
- params[:text],
- params[:blob]
- )
-
- 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
-end
diff --git a/app/models/contact_form.rb b/app/models/contact_form.rb
new file mode 100644
index 00000000000..4e518e59891
--- /dev/null
+++ b/app/models/contact_form.rb
@@ -0,0 +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: [: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: [: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
+
+ 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/contact/_form.html.haml b/app/views/contact/_form.html.haml
new file mode 100644
index 00000000000..c284df56682
--- /dev/null
+++ b/app/views/contact/_form.html.haml
@@ -0,0 +1,60 @@
+= 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?
+ = render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email' }) do |c|
+ - c.with_label { ContactForm.human_attribute_name(form.for_admin? ? :email_pro : :email) }
+
+ %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 :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
+
+ - if link.present?
+ .fr-ml-3w{ id: "card-#{question_type}",
+ class: class_names('hidden' => question_type != form.question_type),
+ "aria-hidden": question_type != form.question_type,
+ data: { "contact-target": "content" } }
+ = render Dsfr::CalloutComponent.new(title: t('.our_answer')) do |c|
+ - c.with_html_body do
+ -# 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?
+ = 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': ContactForm::TYPE_AMELIORATION } }
+ = t('.notice_pj_product')
+ %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.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
+
+ .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/contact/admin.html.haml b/app/views/contact/admin.html.haml
new file mode 100644
index 00000000000..1771256fc49
--- /dev/null
+++ b/app/views/contact/admin.html.haml
@@ -0,0 +1,12 @@
+- content_for(:title, t('.contact_team'))
+- content_for :footer do
+ = render partial: "root/footer"
+
+#contact-form
+ .fr-container
+ %h1
+ = t('.contact_team')
+
+ .fr-highlight= t('.admin_intro_html', contact_path: contact_path)
+
+ = render partial: "form", object: @form
diff --git a/app/views/contact/index.html.haml b/app/views/contact/index.html.haml
new file mode 100644
index 00000000000..0a33c87f180
--- /dev/null
+++ b/app/views/contact/index.html.haml
@@ -0,0 +1,12 @@
+- content_for(:title, t('.contact'))
+- content_for :footer do
+ = render partial: "root/footer"
+
+#contact-form
+ .fr-container
+ %h1
+ = t('.contact')
+
+ .fr-highlight= t('.intro_html')
+
+ = render partial: "form", object: @form
diff --git a/app/views/support/admin.html.haml b/app/views/support/admin.html.haml
deleted file mode 100644
index 28a53bc7243..00000000000
--- a/app/views/support/admin.html.haml
+++ /dev/null
@@ -1,49 +0,0 @@
-- content_for(:title, 'Contact')
-
-#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])
-
- = 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'
diff --git a/app/views/support/index.html.haml b/app/views/support/index.html.haml
deleted file mode 100644
index 26727bcbeb3..00000000000
--- a/app/views/support/index.html.haml
+++ /dev/null
@@ -1,77 +0,0 @@
-- content_for(:title, t('.contact'))
-- content_for :footer do
- = render partial: "root/footer"
-
-#contact-form
- .fr-container
- %h1
- = t('.contact')
-
- = form_tag contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :support } do
-
- .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'
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 ( ) 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 ( ) 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/contact/en.yml b/config/locales/views/contact/en.yml
new file mode 100644
index 00000000000..c0815a28707
--- /dev/null
+++ b/config/locales/views/contact/en.yml
@@ -0,0 +1,73 @@
+en:
+ activerecord:
+ attributes:
+ contact_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:
+ contact_form:
+ invalid_email_format: 'is not valid'
+ contact:
+ form:
+ your_question: Your question
+ 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.'
+ 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).
' + 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.
' + product: + question: I have an idea to improve the website + answer_html: + 'Got an idea? Please check our enhancement dashboard :
+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: + 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.
' + contact_team: Contact our team + admin_question: + question: I have a question about %{app_name} + admin_demande_rdv: + question: I request an appointment for an online presentation of %{app_name} + admin_soucis: + question: I am facing a technical issue on %{app_name} + admin_suggestion_produit: + question: I have a suggestion for an evolution + admin_demande_compte: + 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/contact/fr.yml b/config/locales/views/contact/fr.yml new file mode 100644 index 00000000000..6d5b3d62991 --- /dev/null +++ b/config/locales/views/contact/fr.yml @@ -0,0 +1,74 @@ +fr: + activerecord: + attributes: + contact_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: + contact_form: + invalid_email_format: 'est invalide' + contact: + 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. + 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.' + 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).
' + 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.
' + 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 :
+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: + 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.
' + contact_team: Contactez notre équipe + admin_question: + question: J’ai une question sur %{app_name} + admin_demande_rdv: + question: Demande de RDV pour une présentation à distance de %{app_name} + admin_soucis: + question: J’ai un problème technique avec %{app_name} + admin_suggestion_produit: + question: J’ai une proposition d’évolution + admin_demande_compte: + 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é. diff --git a/config/locales/views/support/en.yml b/config/locales/views/support/en.yml deleted file mode 100644 index 97012927680..00000000000 --- a/config/locales/views/support/en.yml +++ /dev/null @@ -1,53 +0,0 @@ -en: - 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' - your_question: Your question - 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." - 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).
" - 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.
" - product: - question: I have an idea to improve the website - answer_html: "Got an idea? Please check our enhancement dashboard :
-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.
-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.
" - contact_team: Contact our team - pro_phone_number: Professional phone number (direct line) - pro_mail: Professional email address - admin question: - question: I have a question about %{app_name} - admin demande rdv: - question: I request an appointment for an online presentation of %{app_name} - admin soucis: - question: I am facing a technical issue on %{app_name} - admin suggestion produit: - question: I have a suggestion for an evolution - admin demande compte: - question: I want to open an admin account with an Orange, Wanadoo, etc. email - admin autre: - question: Other topic diff --git a/config/locales/views/support/fr.yml b/config/locales/views/support/fr.yml deleted file mode 100644 index 02f520625f9..00000000000 --- a/config/locales/views/support/fr.yml +++ /dev/null @@ -1,53 +0,0 @@ -fr: - 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' - 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. - 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." - 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).
" - 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.
" - 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 :
-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.
-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.
" - 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: - question: J’ai une question sur %{app_name} - admin demande rdv: - question: Demande de RDV pour une présentation à distance de %{app_name} - admin soucis: - question: J’ai un problème technique avec %{app_name} - admin suggestion produit: - question: J’ai une proposition d’évolution - admin demande compte: - question: Je souhaite ouvrir un compte administrateur avec un email Orange, Wanadoo, etc. - admin autre: - question: Autre sujet 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/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/controllers/contact_controller_spec.rb b/spec/controllers/contact_controller_spec.rb new file mode 100644 index 00000000000..44bafc57de6 --- /dev/null +++ b/spec/controllers/contact_controller_spec.rb @@ -0,0 +1,257 @@ +describe ContactController, question_type: :controller do + render_views + + context 'signed in' do + before do + sign_in user + end + + let(:user) { create(:user) } + + it 'should not have email field' do + get :index + + expect(response.status).to eq(200) + expect(response.body).not_to have_content("Votre adresse email") + end + + describe "with dossier" do + let(:user) { dossier.user } + let(:dossier) { create(:dossier) } + + it 'should fill dossier_id' do + get :index, params: { dossier_id: dossier.id } + + expect(response.status).to eq(200) + expect(response.body).to include((dossier.id).to_s) + end + end + + describe "with tag" do + let(:tag) { 'yolo' } + + it 'should fill tags' do + get :index, params: { tags: [tag] } + + expect(response.status).to eq(200) + expect(response.body).to include(tag) + end + end + + describe "with multiple tags" do + let(:tags) { ['yolo', 'toto'] } + + it 'should fill tags' do + get :index, params: { tags: tags } + + expect(response.status).to eq(200) + 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: { contact_form: params } + end + + context "when invisible captcha is ignored" do + 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 \ + 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 + end + + context 'when a drafted dossier is mentionned' do + let(:dossier) { create(:dossier) } + let(:user) { dossier.user } + + let(:params) do + { + dossier_id: dossier.id, + question_type: ContactForm::TYPE_INSTRUCTION, + subject: 'bonjour', + text: 'un message' + } + end + + it 'creates a conversation on HelpScout' do + expect { subject }.to \ + change(Commentaire, :count).by(0).and \ + 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 + end + end + + context 'when a submitted dossier is mentionned' do + let(:dossier) { create(:dossier, :en_construction) } + let(:user) { dossier.user } + + let(:params) do + { + dossier_id: dossier.id, + question_type: ContactForm::TYPE_INSTRUCTION, + subject: 'bonjour', + text: 'un message' + } + end + + it 'posts the message to the dossier messagerie' do + expect { subject }.to change(Commentaire, :count).by(1) + assert_no_enqueued_jobs(only: HelpscoutCreateConversationJob) + + expect(Commentaire.last.email).to eq(user.email) + expect(Commentaire.last.dossier).to eq(dossier) + expect(Commentaire.last.body).to include('[bonjour]') + expect(Commentaire.last.body).to include('un message') + + expect(flash[:notice]).to match('Votre message a été envoyé sur la messagerie de votre dossier.') + expect(response).to redirect_to messagerie_dossier_path(dossier) + end + end + end + + context "when invisible captcha is filled" do + subject do + post :create, params: { + contact_form: { subject: 'bonjour', text: 'un message', question_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')) + end + end + end + end + + context 'signed out' do + describe "with dossier" do + it 'should have email field' do + get :index + + expect(response.status).to eq(200) + expect(response.body).to have_text("Votre adresse email") + end + end + + describe "with dossier" do + let(:tag) { 'yolo' } + + it 'should fill tags' do + get :index, params: { tags: [tag] } + + expect(response.status).to eq(200) + expect(response.body).to include(tag) + end + end + + describe 'send form' do + subject do + post :create, params: { contact_form: params } + end + + 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 \ + 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 + 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 + 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 + + context 'create' do + subject do + post :create, params: { contact_form: params } + end + + let(:params) { { for_admin: "true", email: "email@pro.fr", subject: 'bonjour', text: 'un message', question_type: 'admin question', phone: '06' } } + + 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 + + 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 + + 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 +end diff --git a/spec/controllers/support_controller_spec.rb b/spec/controllers/support_controller_spec.rb deleted file mode 100644 index 281c7e4a3dc..00000000000 --- a/spec/controllers/support_controller_spec.rb +++ /dev/null @@ -1,187 +0,0 @@ -describe SupportController, type: :controller do - render_views - - context 'signed in' do - before do - sign_in user - end - - let(:user) { create(:user) } - - it 'should not have email field' do - get :index - - expect(response.status).to eq(200) - expect(response.body).not_to have_content("Email *") - end - - describe "with dossier" do - let(:user) { dossier.user } - let(:dossier) { create(:dossier) } - - it 'should fill dossier_id' do - get :index, params: { dossier_id: dossier.id } - - expect(response.status).to eq(200) - expect(response.body).to include((dossier.id).to_s) - end - end - - describe "with tag" do - let(:tag) { 'yolo' } - - it 'should fill tags' do - get :index, params: { tags: [tag] } - - expect(response.status).to eq(200) - expect(response.body).to include(tag) - end - end - - describe "with multiple tags" do - let(:tags) { ['yolo', 'toto'] } - - it 'should fill tags' do - get :index, params: { tags: tags } - - expect(response.status).to eq(200) - expect(response.body).to include(tags.join(',')) - end - end - - describe "send form" do - subject do - post :create, params: params - end - - context "when invisible captcha is ignored" do - let(:params) { { subject: 'bonjour', text: 'un message' } } - - it 'creates a conversation on HelpScout' do - expect { subject }.to \ - change(Commentaire, :count).by(0).and \ - have_enqueued_job(HelpscoutCreateConversationJob).with(hash_including(params)) - - expect(flash[:notice]).to match('Votre message a été envoyé.') - expect(response).to redirect_to root_path(formulaire_contact_general_submitted: true) - end - - context 'when a drafted dossier is mentionned' do - let(:dossier) { create(:dossier) } - let(:user) { dossier.user } - - subject do - post :create, params: { - dossier_id: dossier.id, - type: Helpscout::FormAdapter::TYPE_INSTRUCTION, - subject: 'bonjour', - text: 'un message' - } - end - - 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)) - - expect(flash[:notice]).to match('Votre message a été envoyé.') - expect(response).to redirect_to root_path(formulaire_contact_general_submitted: true) - end - end - - context 'when a submitted dossier is mentionned' do - let(:dossier) { create(:dossier, :en_construction) } - let(:user) { dossier.user } - - subject do - post :create, params: { - dossier_id: dossier.id, - type: Helpscout::FormAdapter::TYPE_INSTRUCTION, - subject: 'bonjour', - text: 'un message' - } - end - - it 'posts the message to the dossier messagerie' do - expect { subject }.to change(Commentaire, :count).by(1) - assert_no_enqueued_jobs(only: HelpscoutCreateConversationJob) - - expect(Commentaire.last.email).to eq(user.email) - expect(Commentaire.last.dossier).to eq(dossier) - expect(Commentaire.last.body).to include('[bonjour]') - expect(Commentaire.last.body).to include('un message') - - expect(flash[:notice]).to match('Votre message a été envoyé sur la messagerie de votre dossier.') - expect(response).to redirect_to messagerie_dossier_path(dossier) - end - end - end - - context "when invisible captcha is filled" do - let(:params) { { subject: 'bonjour', text: 'un message', InvisibleCaptcha.honeypots.sample => 'boom' } } - 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')) - end - end - end - end - - context 'signed out' do - describe "with dossier" do - it 'should have email field' do - get :index - - expect(response.status).to eq(200) - expect(response.body).to have_text("Email") - end - end - - describe "with dossier" do - let(:tag) { 'yolo' } - - it 'should fill tags' do - get :index, params: { tags: [tag] } - - expect(response.status).to eq(200) - expect(response.body).to include(tag) - end - end - end - - context 'contact admin' do - subject do - post :create, params: params - end - - let(:params) { { admin: "true", email: "email@pro.fr", subject: 'bonjour', text: 'un message' } } - - 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(flash[:notice]).to match('Votre message a été envoyé.') - 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) } - - 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) - end - end - end - - describe "when invisible captcha is filled" do - let(:params) { super().merge(InvisibleCaptcha.honeypots.sample => 'boom') } - - 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/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 diff --git a/spec/jobs/helpscout_create_conversation_job_spec.rb b/spec/jobs/helpscout_create_conversation_job_spec.rb index 1220e538d64..5b45084f4d9 100644 --- a/spec/jobs/helpscout_create_conversation_job_spec.rb +++ b/spec/jobs/helpscout_create_conversation_job_spec.rb @@ -1,64 +1,86 @@ 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(:question_type) { "lost" } + let(:phone) { nil } + let(:contact_form) { create(:contact_form, email:, subject: subject_text, text:, tags:, phone:, question_type:) } describe '#perform' do - 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) + 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 phone.present? + end - described_class.perform_now(**args) + subject { + described_class.perform_now(contact_form) + } + + 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", 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") - } - + context 'when a file is attached' do before do - allow(blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: pending, safe?: safe)) + 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 { - described_class.perform_now(blob_id: blob.id, **args) - }.to have_enqueued_job(described_class).with(blob_id: blob.id, **args) + 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 } - - 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) + before do + allow_any_instance_of(ActiveStorage::Blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: false, safe?: true)) + end - described_class.perform_now(blob_id: blob.id, **args) + it 'sends the form with the file' do + subject + 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 } - - 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) + before do + allow_any_instance_of(ActiveStorage::Blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: false, safe?: false)) + end - described_class.perform_now(blob_id: blob.id, **args) + it 'ignores 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