Merge pull request #10644 from colinux/fix-helpscout-invalid-email

ETQ utilisateur, le form de contact détecte les typos d'email et valide les champs avant de l'envoyer à HS
This commit is contained in:
Colin Darie 2024-07-31 17:00:35 +00:00 committed by GitHub
commit 3b82621229
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 800 additions and 768 deletions

View file

@ -127,6 +127,8 @@ module Dsfr
end end
end end
def hint? = hint.present?
def password? def password?
false false
end end
@ -142,15 +144,6 @@ module Dsfr
def hintable? def hintable?
false false
end 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 end
end end

View file

@ -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]}]<br><br>#{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

View file

@ -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]}]<br><br>#{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

View file

@ -74,7 +74,7 @@ module ApplicationHelper
tags, type, dossier_id = options.values_at(:tags, :type, :dossier_id) tags, type, dossier_id = options.values_at(:tags, :type, :dossier_id)
options.except!(: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 link_to title, contact_url(params), options
end end

View file

@ -1,7 +1,7 @@
import { ApplicationController } from './application_controller'; import { ApplicationController } from './application_controller';
import { hide, show } from '@utils'; import { hide, show } from '@utils';
export class SupportController extends ApplicationController { export class ContactController extends ApplicationController {
static targets = ['inputRadio', 'content']; static targets = ['inputRadio', 'content'];
declare readonly inputRadioTargets: HTMLInputElement[]; declare readonly inputRadioTargets: HTMLInputElement[];

View file

@ -8,14 +8,49 @@ class HelpscoutCreateConversationJob < ApplicationJob
retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10 retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10
def perform(blob_id: nil, **args) attr_reader :contact_form
if blob_id.present? attr_reader :api
blob = ActiveStorage::Blob.find(blob_id)
raise FileNotScannedYetError if blob.virus_scanner.pending?
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 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
end end

View file

@ -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

View file

@ -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

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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'

View file

@ -102,6 +102,7 @@ ignore_unused:
- 'activerecord.models.*' - 'activerecord.models.*'
- 'activerecord.attributes.*' - 'activerecord.attributes.*'
- 'activemodel.attributes.map_filter.*' - 'activemodel.attributes.map_filter.*'
- 'activemodel.attributes.helpscout/form.*'
- 'activerecord.errors.*' - 'activerecord.errors.*'
- 'errors.messages.blank' - 'errors.messages.blank'
- 'errors.messages.content_type_invalid' - 'errors.messages.content_type_invalid'

View file

@ -52,9 +52,6 @@ en:
asterisk_html: "Fields marked by an asterisk ( <svg aria-label='required' class='icon mandatory' height='10' role='img' viewBox='0 0 1200 1200' width='10' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><desc>required</desc><path d='M489.838 29.354v443.603L68.032 335.894 0 545.285l421.829 137.086-260.743 358.876 178.219 129.398L600.048 811.84l260.673 358.806 178.146-129.398-260.766-358.783L1200 545.379l-68.032-209.403-421.899 137.07V29.443H489.84l-.002-.089z'></path></svg> ) are mandatory." asterisk_html: "Fields marked by an asterisk ( <svg aria-label='required' class='icon mandatory' height='10' role='img' viewBox='0 0 1200 1200' width='10' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><desc>required</desc><path d='M489.838 29.354v443.603L68.032 335.894 0 545.285l421.829 137.086-260.743 358.876 178.219 129.398L600.048 811.84l260.673 358.806 178.146-129.398-260.766-358.783L1200 545.379l-68.032-209.403-421.899 137.07V29.443H489.84l-.002-.089z'></path></svg> ) are mandatory."
mandatory_champs: All fields are mandatory. mandatory_champs: All fields are mandatory.
no_mandatory: (optional) no_mandatory: (optional)
file_number: File number
subject: Subject
message: Message
send_mail: Send message send_mail: Send message
new_tab: New tab new_tab: New tab
helpers: helpers:

View file

@ -43,9 +43,6 @@ fr:
asterisk_html: "Les champs suivis dun astérisque ( <svg aria-label='obligatoire' class='icon mandatory' height='10' role='img' viewBox='0 0 1200 1200' width='10' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><desc>obligatoire</desc><path d='M489.838 29.354v443.603L68.032 335.894 0 545.285l421.829 137.086-260.743 358.876 178.219 129.398L600.048 811.84l260.673 358.806 178.146-129.398-260.766-358.783L1200 545.379l-68.032-209.403-421.899 137.07V29.443H489.84l-.002-.089z'></path></svg> ) sont obligatoires." asterisk_html: "Les champs suivis dun astérisque ( <svg aria-label='obligatoire' class='icon mandatory' height='10' role='img' viewBox='0 0 1200 1200' width='10' xml:space='preserve' xmlns='http://www.w3.org/2000/svg'><desc>obligatoire</desc><path d='M489.838 29.354v443.603L68.032 335.894 0 545.285l421.829 137.086-260.743 358.876 178.219 129.398L600.048 811.84l260.673 358.806 178.146-129.398-260.766-358.783L1200 545.379l-68.032-209.403-421.899 137.07V29.443H489.84l-.002-.089z'></path></svg> ) sont obligatoires."
mandatory_champs: Tous les champs sont obligatoires. mandatory_champs: Tous les champs sont obligatoires.
no_mandatory: (facultatif) no_mandatory: (facultatif)
file_number: Numéro de dossier
subject: Sujet
message: Message
send_mail: Envoyer le message send_mail: Envoyer le message
new_tab: "Nouvel onglet" new_tab: "Nouvel onglet"
helpers: helpers:

View file

@ -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:
'<p>Contact us via this form and we will answer you as quickly as possible.</p>
<p>Make sure you provide all the required information so we can help you in the best way.</p>'
procedure_info:
question: I've encountered a problem while completing my application
answer_html:
'<p>Are you sure that all the mandatory fields (<span class= mandatory> * </span>) are properly filled?
<p>If you have questions about the information requested, <a href="%{link_procedure_info}">contact the service in charge of the procedure (FAQ)</a>.</p>'
instruction_info:
question: I have a question about the instruction of my application
answer_html:
'<p>If you have questions about the instruction of your application (response delay for example), <a href="%{link_instruction_info}">contact directly the instructor via our mail system (FAQ)</a>.</p>
<p>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.</p>'
product:
question: I have an idea to improve the website
answer_html:
'<p>Got an idea? <a href="%{link_product}" rel="noopener noreferrer" target="_blank" title="Please check our enhancement dashboard - New tab">Please check our enhancement dashboard</a> :</p>
<ul><li>Vote for your priority improvements</li>
<li>Share your own ideas</li></ul>'
lost_user:
question: I am having trouble finding the procedure I am looking for
answer_html:
'<p>We invite you to contact the administration in charge of the procedure so they can provide you the link.
It should look like this: <code>%{base_url}/commencer/NOM_DE_LA_DEMARCHE</code>.</p>
<p>You can <a href="%{link_lost_user}">find here the most popular procedures (licence, detr, etc.)</a>.</p>'
other:
question: Other topic
admin:
admin_intro_html:
'<p>As an administration, you can contact us through this form. We''ll answer you as quickly as possibly by e-mail or phone.</p>
<p><strong>Caution, this form is dedicated to public bodies only.</strong>
It does not concern individuals, companies nor associations (except those recognised of public utility). If you belong to one of these categories, contact us <a href="%{contact_path}">here</a>.</p>'
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.

View file

@ -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 lendroit à 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:
'<p>Contactez-nous via ce formulaire et nous vous répondrons dans les plus brefs délais.</p>
<p>Pensez bien à nous donner le plus dinformations possible pour que nous puissions vous aider au mieux.</p>'
procedure_info:
question: Jai un problème lors du remplissage de mon dossier
answer_html:
'<p>Avez-vous bien vérifié que tous les champs obligatoires (<span class= mandatory> * </span>) sont remplis ?
<p>Si vous avez des questions sur les informations à saisir, <a href="%{link_procedure_info}">contactez les services en charge de la démarche (FAQ)</a>.</p>'
instruction_info:
question: Jai une question sur linstruction de mon dossier
answer_html:
'<p>Si vous avez des questions sur linstruction de votre dossier (par exemple sur les délais), nous vous invitons à <a href="%{link_instruction_info}">contacter directement les services qui instruisent votre dossier par votre messagerie (FAQ)</a>.</p>
<p>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 linstruction de votre dossier.</p>'
product:
question: Jai une idée damélioration pour votre site
answer_html:
'<p>Une idée ? Pensez à <a href="%{link_product}" rel="noopener noreferrer" target="_blank" title="Consulter notre tableau de bord des améliorations - Nouvel onglet">consulter notre tableau de bord des améliorations</a> :</p>
<ul><li>Votez pour vos améliorations prioritaires,</li>
<li>Proposez votre propre idée.</li></ul>'
lost_user:
question: Je ne trouve pas la démarche que je veux faire
answer_html:
'<p>Nous vous invitons à contacter ladministration en charge de votre démarche pour quelle vous indique le lien à suivre. Celui-ci devrait ressembler à cela : <code>%{base_url}/commencer/NOM_DE_LA_DEMARCHE</code>.</p>
<p>Vous pouvez aussi <a href="%{link_lost_user}">consulter ici la liste de nos démarches les plus fréquentes (permis, detr, etc.)</a>.</p>'
other:
question: Autre sujet
admin:
admin_intro_html:
'<p>En tant quadministration, vous pouvez nous contactez via ce formulaire. Nous vous répondrons dans les plus brefs délais, par email ou par téléphone.</p>
<p><strong>Attention, ce formulaire est réservé uniquement aux organismes publics.</strong>
Il ne concerne ni les particuliers, ni les entreprises, ni les associations (sauf celles reconnues dutilité publique). Si c''est votre cas, rendez-vous sur notre
<a href="%{contact_path}">formulaire de contact public</a>.</p>'
contact_team: Contactez notre équipe
admin_question:
question: Jai une question sur %{app_name}
admin_demande_rdv:
question: Demande de RDV pour une présentation à distance de %{app_name}
admin_soucis:
question: Jai un problème technique avec %{app_name}
admin_suggestion_produit:
question: Jai 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é.

View file

@ -1,53 +0,0 @@
en:
support:
index:
contact: Contact
intro_html:
'<p>Contact us via this form and we will answer you as quickly as possible.</p>
<p>Make sure you provide all the required information so we can help you in the best way.</p>'
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: "<p>Are you sure that all the mandatory fields (<span class= mandatory> * </span>) are properly filled?
<p>If you have questions about the information requested, <a href=\"%{link_procedure_info}\">contact the service in charge of the procedure (FAQ)</a>.</p>"
instruction_info:
question: I have a question about the instruction of my application
answer_html: "<p>If you have questions about the instruction of your application (response delay for example), <a href=\"%{link_instruction_info}\">contact directly the instructor via our mail system (FAQ)</a>.</p>
<p>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.</p>"
product:
question: I have an idea to improve the website
answer_html: "<p>Got an idea? <a href=\"%{link_product}\" rel=\"noopener noreferrer\" target=\"_blank\" title=\"Please check our enhancement dashboard - New tab\">Please check our enhancement dashboard</a> :</p>
<ul><li>Vote for your priority improvements</li>
<li>Share your own ideas</li></ul>"
lost_user:
question: I am having trouble finding the procedure I am looking for
answer_html: "<p>We invite you to contact the administration in charge of the procedure so they can provide you the link.
It should look like this: <code>%{base_url}/commencer/NOM_DE_LA_DEMARCHE</code>.</p>
<p>You can <a href=\"%{link_lost_user}\">find here the most popular procedures (licence, detr, etc.)</a>.</p>"
other:
question: Other topic
admin:
your_question: Your question
admin_intro_html: "<p>As an administration, you can contact us through this form. We'll answer you as quickly as possibly by e-mail or phone.</p>
<p><strong>Caution, this form is dedicated to public bodies only.</strong>
It does not concern individuals, companies nor associations (except those recognised of public utility). If you belong to one of these categories, contact us <a href=\"%{contact_path}\">here</a>.</p>"
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

View file

@ -1,53 +0,0 @@
fr:
support:
index:
contact: Contact
intro_html:
'<p>Contactez-nous via ce formulaire et nous vous répondrons dans les plus brefs délais.</p>
<p>Pensez bien à nous donner le plus dinformations possible pour que nous puissions vous aider au mieux.</p>'
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 lendroit à 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: Jai un problème lors du remplissage de mon dossier
answer_html: "<p>Avez-vous bien vérifié que tous les champs obligatoires (<span class= mandatory> * </span>) sont remplis ?
<p>Si vous avez des questions sur les informations à saisir, <a href=\"%{link_procedure_info}\">contactez les services en charge de la démarche (FAQ)</a>.</p>"
instruction_info:
question: Jai une question sur linstruction de mon dossier
answer_html: "<p>Si vous avez des questions sur linstruction de votre dossier (par exemple sur les délais), nous vous invitons à <a href=\"%{link_instruction_info}\">contacter directement les services qui instruisent votre dossier par votre messagerie (FAQ)</a>.</p>
<p>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 linstruction de votre dossier.</p>"
product:
question: Jai une idée damélioration pour votre site
answer_html: "<p>Une idée ? Pensez à <a href=\"%{link_product}\" rel=\"noopener noreferrer\" target=\"_blank\" title=\"Consulter notre tableau de bord des améliorations - Nouvel onglet\">consulter notre tableau de bord des améliorations</a> :</p>
<ul><li>Votez pour vos améliorations prioritaires,</li>
<li>Proposez votre propre idée.</li></ul>"
lost_user:
question: Je ne trouve pas la démarche que je veux faire
answer_html: "<p>Nous vous invitons à contacter ladministration en charge de votre démarche pour quelle vous indique le lien à suivre. Celui-ci devrait ressembler à cela : <code>%{base_url}/commencer/NOM_DE_LA_DEMARCHE</code>.</p>
<p>Vous pouvez aussi <a href=\"%{link_lost_user}\">consulter ici la liste de nos démarches les plus fréquentes (permis, detr, etc.)</a>.</p>"
other:
question: Autre sujet
admin:
your_question: Votre question
admin_intro_html: "<p>En tant quadministration, vous pouvez nous contactez via ce formulaire. Nous vous répondrons dans les plus brefs délais, par email ou par téléphone.</p>
<p><strong>Attention, ce formulaire est réservé uniquement aux organismes publics.</strong>
Il ne concerne ni les particuliers, ni les entreprises, ni les associations (sauf celles reconnues dutilité publique). Si c'est votre cas, rendez-vous sur notre
<a href=\"%{contact_path}\">formulaire de contact public</a>.</p>"
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: Jai une question sur %{app_name}
admin demande rdv:
question: Demande de RDV pour une présentation à distance de %{app_name}
admin soucis:
question: Jai un problème technique avec %{app_name}
admin suggestion produit:
question: Jai 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

View file

@ -224,10 +224,10 @@ Rails.application.routes.draw do
get "suivi" => "root#suivi" get "suivi" => "root#suivi"
post "save_locale" => "root#save_locale" post "save_locale" => "root#save_locale"
get "contact", to: "support#index" get "contact", to: "contact#index"
post "contact", to: "support#create" 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 "mentions-legales", to: "static_pages#legal_notice"
get "declaration-accessibilite", to: "static_pages#accessibility_statement" get "declaration-accessibilite", to: "static_pages#accessibility_statement"

View file

@ -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

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "pg_buffercache" enable_extension "pg_buffercache"
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
@ -315,6 +315,21 @@ ActiveRecord::Schema[7.0].define(version: 2024_07_16_091043) do
t.index ["instructeur_id"], name: "index_commentaires_on_instructeur_id" t.index ["instructeur_id"], name: "index_commentaires_on_instructeur_id"
end 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| create_table "contact_informations", force: :cascade do |t|
t.text "adresse", null: false t.text "adresse", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
@ -1229,6 +1244,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_07_16_091043) do
add_foreign_key "commentaires", "dossiers" add_foreign_key "commentaires", "dossiers"
add_foreign_key "commentaires", "experts" add_foreign_key "commentaires", "experts"
add_foreign_key "commentaires", "instructeurs" add_foreign_key "commentaires", "instructeurs"
add_foreign_key "contact_forms", "users"
add_foreign_key "contact_informations", "groupe_instructeurs" add_foreign_key "contact_informations", "groupe_instructeurs"
add_foreign_key "dossier_assignments", "dossiers" add_foreign_key "dossier_assignments", "dossiers"
add_foreign_key "dossier_batch_operations", "batch_operations" add_foreign_key "dossier_batch_operations", "batch_operations"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,63 +1,85 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe HelpscoutCreateConversationJob, type: :job do 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 describe '#perform' do
context 'when blob_id is not present' do before do
it 'sends the form without a file' do allow(Helpscout::API).to receive(:new).and_return(api)
form_adapter = double('Helpscout::FormAdapter') allow(api).to receive(:create_conversation)
allow(Helpscout::FormAdapter).to receive(:new).with(hash_including(args.merge(blob: nil))).and_return(form_adapter) .and_return(double(
expect(form_adapter).to receive(:send_form) 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
end end
context 'when blob_id is present' do context 'when a file is attached' do
let(:blob) {
ActiveStorage::Blob.create_and_upload!(io: StringIO.new("toto"), filename: "toto.png")
}
before 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 end
context 'when the file has not been scanned yet' do context 'when the file has not been scanned yet' do
let(:pending) { true } before do
let(:safe) { false } allow_any_instance_of(ActiveStorage::Blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: true, safe?: false))
end
it 'reenqueue job' do it 'reenqueues job' do
expect { expect { subject }.to have_enqueued_job(described_class).with(contact_form)
described_class.perform_now(blob_id: blob.id, **args)
}.to have_enqueued_job(described_class).with(blob_id: blob.id, **args)
end end
end end
context 'when the file is safe' do context 'when the file is safe' do
let(:pending) { false } before do
let(:safe) { true } 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
form_adapter = double('Helpscout::FormAdapter') subject
allow(Helpscout::FormAdapter).to receive(:new).with(hash_including(args.merge(blob:))).and_return(form_adapter) expect(api).to have_received(:create_conversation).with(email, subject_text, text, contact_form.piece_jointe)
allow(form_adapter).to receive(:send_form)
described_class.perform_now(blob_id: blob.id, **args)
end end
end end
context 'when the file is not safe' do context 'when the file is not safe' do
let(:pending) { false } before do
let(:safe) { false } allow_any_instance_of(ActiveStorage::Blob).to receive(:virus_scanner).and_return(double('VirusScanner', pending?: false, 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)
end end
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
end end

View file

@ -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