Merge pull request #4996 from betagouv/feat/user-search-inside-dossiers

#2191 - Permettre aux usagers de rechercher dans le contenu de leurs dossiers
This commit is contained in:
Paul Chavard 2020-04-09 09:52:24 +02:00 committed by GitHub
commit d2811bdf73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 191 additions and 25 deletions

View file

@ -219,13 +219,21 @@ module Users
end end
def recherche def recherche
@dossier_id = params[:dossier_id] @search_terms = params[:q]
dossier = current_user.dossiers.find_by(id: @dossier_id) return redirect_to dossiers_path if @search_terms.blank?
if dossier @dossiers = DossierSearchService.matching_dossiers_for_user(@search_terms, current_user).page(page)
redirect_to url_for_dossier(dossier)
if @dossiers.present?
# we need the page condition when accessing page n with n>1 when the page has only 1 result
# in order to avoid an unpleasant redirection when changing page
if @dossiers.count == 1 && page == 1
redirect_to url_for_dossier(@dossiers.first)
else
render :index
end
else else
flash.alert = "Vous navez pas de dossier avec le nº #{@dossier_id}." flash.alert = "Vous navez pas de dossiers contenant « #{@search_terms} »."
redirect_to dossiers_path redirect_to dossiers_path
end end
end end

View file

@ -4,6 +4,11 @@ class DossierSearchService
.presence || dossier_by_full_text_for_instructeur(search_terms, instructeur) .presence || dossier_by_full_text_for_instructeur(search_terms, instructeur)
end end
def self.matching_dossiers_for_user(search_terms, user)
dossier_by_exact_id_for_user(search_terms, user)
.presence || dossier_by_full_text_for_user(search_terms, user.dossiers) || dossier_by_full_text_for_user(search_terms, user.dossiers_invites)
end
private private
def self.dossier_by_exact_id_for_instructeur(search_terms, instructeur) def self.dossier_by_exact_id_for_instructeur(search_terms, instructeur)
@ -26,6 +31,24 @@ class DossierSearchService
false false
end end
def self.dossier_by_full_text_for_user(search_terms, dossiers)
ts_vector = "to_tsvector('french', search_terms)"
ts_query = "to_tsquery('french', #{Dossier.connection.quote(to_tsquery(search_terms))})"
dossiers
.where("#{ts_vector} @@ #{ts_query}")
.order("COALESCE(ts_rank(#{ts_vector}, #{ts_query}), 0) DESC")
end
def self.dossier_by_exact_id_for_user(search_terms, user)
id = search_terms.to_i
if id != 0 && id_compatible?(id) # Sometimes user is searching dossiers with a big number (ex: SIRET), ActiveRecord can't deal with them and throws ActiveModel::RangeError. id_compatible? prevents this.
Dossier.where(id: user.dossiers.where(id: id) + user.dossiers_invites.where(id: id)).distinct
else
Dossier.none
end
end
def self.dossier_by_full_text_for_instructeur(search_terms, instructeur) def self.dossier_by_full_text_for_instructeur(search_terms, instructeur)
ts_vector = "to_tsvector('french', search_terms || private_search_terms)" ts_vector = "to_tsvector('french', search_terms || private_search_terms)"
ts_query = "to_tsquery('french', #{Dossier.connection.quote(to_tsquery(search_terms))})" ts_query = "to_tsquery('french', #{Dossier.connection.quote(to_tsquery(search_terms))})"

View file

@ -45,20 +45,11 @@
%ul.header-right-content %ul.header-right-content
- if nav_bar_profile == :instructeur && instructeur_signed_in? - if nav_bar_profile == :instructeur && instructeur_signed_in?
%li %li
.header-search{ role: 'search' } = render partial: 'layouts/search_dossiers_form', locals: { search_endpoint: instructeur_recherche_path }
= form_tag instructeur_recherche_path, method: :get, class: "form" do
= text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: "Rechercher un dossier"
%button{ title: "Rechercher" }
= image_tag "icons/search-blue.svg", alt: ''
- if nav_bar_profile == :user && user_signed_in? && current_user.dossiers.count > 2 - if nav_bar_profile == :user && user_signed_in? && current_user.dossiers.count > 2
%li %li
.header-search{ role: 'search' } = render partial: 'layouts/search_dossiers_form', locals: { search_endpoint: recherche_dossiers_path }
= form_tag recherche_dossiers_path, method: :post, class: "form" do
= label_tag :dossier_id, "Numéro de dossier", class: 'hidden'
= text_field_tag :dossier_id, "", placeholder: "Numéro de dossier"
%button{ title: "Rechercher" }
= image_tag "icons/search-blue.svg", alt: 'Rechercher', 'aria-hidden':'true'
- if instructeur_signed_in? || user_signed_in? - if instructeur_signed_in? || user_signed_in?
%li %li

View file

@ -0,0 +1,6 @@
.header-search{ role: 'search' }
= form_tag "#{search_endpoint}", method: :get, class: "form" do
= label_tag :q, "Numéro de dossier", class: 'hidden'
= text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: "Rechercher un dossier"
%button{ title: "Rechercher" }
= image_tag "icons/search-blue.svg", alt: 'Rechercher', 'aria-hidden':'true'

View file

@ -1,11 +1,16 @@
- content_for(:title, "Dossiers") - if @search_terms.present?
- content_for(:title, "Recherche : #{@search_terms}")
- else
- content_for(:title, "Dossiers")
- content_for :footer do - content_for :footer do
= render partial: "users/dossiers/index_footer" = render partial: "users/dossiers/index_footer"
.dossiers-headers.sub-header .dossiers-headers.sub-header
.container .container
- if @dossiers_invites.count == 0 - if @search_terms.present?
%h1.page-title Résultat de la recherche pour « #{@search_terms} »
- elsif @dossiers_invites.count == 0
%h1.page-title Mes dossiers %h1.page-title Mes dossiers
- else - else

View file

@ -275,7 +275,7 @@ Rails.application.routes.draw do
end end
collection do collection do
post 'recherche' get 'recherche'
end end
end end
resource :feedback, only: [:create] resource :feedback, only: [:create]

View file

@ -1,7 +1,7 @@
describe 'user access to the list of their dossiers' do describe 'user access to the list of their dossiers' do
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:dossier_brouillon) { create(:dossier, user: user) } let!(:dossier_brouillon) { create(:dossier, user: user) }
let!(:dossier_en_construction) { create(:dossier, :en_construction, user: user) } let!(:dossier_en_construction) { create(:dossier, :with_all_champs, :en_construction, user: user) }
let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) } let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) }
let!(:dossier_archived) { create(:dossier, :en_instruction, :archived, user: user) } let!(:dossier_archived) { create(:dossier, :en_instruction, :archived, user: user) }
let(:dossiers_per_page) { 25 } let(:dossiers_per_page) { 25 }
@ -85,13 +85,13 @@ describe 'user access to the list of their dossiers' do
describe "recherche" do describe "recherche" do
context "when the dossier does not exist" do context "when the dossier does not exist" do
before do before do
page.find_by_id('dossier_id').set(10000000) page.find_by_id('q').set(10000000)
click_button("Rechercher") click_button("Rechercher")
end end
it "shows an error message on the dossiers page" do it "shows an error message on the dossiers page" do
expect(current_path).to eq(dossiers_path) expect(current_path).to eq(dossiers_path)
expect(page).to have_content("Vous navez pas de dossier avec le nº 10000000.") expect(page).to have_content("Vous navez pas de dossiers contenant « 10000000 ».")
end end
end end
@ -99,19 +99,19 @@ describe 'user access to the list of their dossiers' do
let!(:dossier_other_user) { create(:dossier) } let!(:dossier_other_user) { create(:dossier) }
before do before do
page.find_by_id('dossier_id').set(dossier_other_user.id) page.find_by_id('q').set(dossier_other_user.id)
click_button("Rechercher") click_button("Rechercher")
end end
it "shows an error message on the dossiers page" do it "shows an error message on the dossiers page" do
expect(current_path).to eq(dossiers_path) expect(current_path).to eq(dossiers_path)
expect(page).to have_content("Vous navez pas de dossier avec le nº #{dossier_other_user.id}.") expect(page).to have_content("Vous navez pas de dossiers contenant « #{dossier_other_user.id} ».")
end end
end end
context "when the dossier belongs to the user" do context "when the dossier belongs to the user" do
before do before do
page.find_by_id('dossier_id').set(dossier_en_construction.id) page.find_by_id('q').set(dossier_en_construction.id)
click_button("Rechercher") click_button("Rechercher")
end end
@ -119,5 +119,34 @@ describe 'user access to the list of their dossiers' do
expect(current_path).to eq(dossier_path(dossier_en_construction)) expect(current_path).to eq(dossier_path(dossier_en_construction))
end end
end end
context "when user search for something inside the dossier" do
let(:dossier_en_construction2) { create(:dossier, :with_all_champs, :en_construction, user: user) }
before do
page.find_by_id('q').set(dossier_en_construction.champs.first.value)
end
context 'when it only matches one dossier' do
before do
click_button("Rechercher")
end
it "redirects to the dossier page" do
expect(current_path).to eq(dossier_path(dossier_en_construction))
end
end
context 'when it matches multiple dossier' do
before do
dossier_en_construction2.champs.first.update(value: dossier_en_construction.champs.first.value)
click_button("Rechercher")
end
it "redirects to the search results" do
expect(current_path).to eq(recherche_dossiers_path)
expect(page).to have_content(dossier_en_construction.id)
expect(page).to have_content(dossier_en_construction2.id)
end
end
end
end end
end end

View file

@ -95,4 +95,108 @@ describe DossierSearchService do
it { expect(subject.size).to eq(1) } it { expect(subject.size).to eq(1) }
end end
end end
describe '#matching_dossiers_for_user' do
subject { liste_dossiers }
let(:liste_dossiers) do
described_class.matching_dossiers_for_user(terms, user_1)
end
let(:user_1) { create(:user, email: 'bidou@clap.fr') }
let(:user_2) { create(:user) }
let(:procedure_1) { create(:procedure, :published) }
let(:procedure_2) { create(:procedure, :published) }
let!(:dossier_0) { create(:dossier, state: Dossier.states.fetch(:brouillon), procedure: procedure_1, user: user_1) }
let!(:dossier_0b) { create(:dossier, state: Dossier.states.fetch(:brouillon), procedure: procedure_1, user: user_2) }
let!(:etablissement_1) { create(:etablissement, entreprise_raison_sociale: 'OCTO Academy', siret: '41636169600051') }
let!(:dossier_1) { create(:dossier, state: Dossier.states.fetch(:en_construction), procedure: procedure_1, user: user_1, etablissement: etablissement_1) }
let!(:etablissement_2) { create(:etablissement, entreprise_raison_sociale: 'Plop octo', siret: '41816602300012') }
let!(:dossier_2) { create(:dossier, state: Dossier.states.fetch(:en_construction), procedure: procedure_1, user: user_1, etablissement: etablissement_2) }
let!(:etablissement_3) { create(:etablissement, entreprise_raison_sociale: 'OCTO Technology', siret: '41816609600051') }
let!(:dossier_3) { create(:dossier, state: Dossier.states.fetch(:en_construction), procedure: procedure_2, user: user_1, etablissement: etablissement_3) }
let!(:dossier_archived) { create(:dossier, state: Dossier.states.fetch(:en_construction), procedure: procedure_1, archived: true, user: user_1) }
describe 'search is empty' do
let(:terms) { '' }
it { expect(subject.size).to eq(0) }
end
describe 'search by dossier id' do
context 'when the user owns the dossier' do
let(:terms) { dossier_0.id.to_s }
it { expect(subject.size).to eq(1) }
end
context 'when the user does not own the dossier' do
let(:terms) { dossier_0b.id.to_s }
it { expect(subject.size).to eq(0) }
end
end
describe 'search brouillon file' do
let(:terms) { 'brouillon' }
it { expect(subject.size).to eq(0) }
end
describe 'search on contact email' do
let(:terms) { 'bidou@clap.fr' }
it { expect(subject.size).to eq(5) }
end
describe 'search on contact name' do
let(:terms) { 'bidou@clap.fr' }
it { expect(subject.size).to eq(5) }
end
describe 'search on SIRET' do
context 'when is part of SIRET' do
let(:terms) { '4181' }
it { expect(subject.size).to eq(2) }
end
context 'when is a complet SIRET' do
let(:terms) { '41816602300012' }
it { expect(subject.size).to eq(1) }
end
end
describe 'search on raison social' do
let(:terms) { 'OCTO' }
it { expect(subject.size).to eq(3) }
end
describe 'search terms surrounded with spurious spaces' do
let(:terms) { ' OCTO ' }
it { expect(subject.size).to eq(3) }
end
describe 'search on multiple fields' do
let(:terms) { 'octo plop' }
it { expect(subject.size).to eq(1) }
end
describe 'search with characters disallowed by the tsquery parser' do
let(:terms) { "'?\\:&!(OCTO) <plop>" }
it { expect(subject.size).to eq(1) }
end
end
end end