diff --git a/app/models/principals/scopes/like.rb b/app/models/principals/scopes/like.rb index b2910e133229..c194ec51050d 100644 --- a/app/models/principals/scopes/like.rb +++ b/app/models/principals/scopes/like.rb @@ -42,11 +42,14 @@ def like(query) s = "%#{query.to_s.downcase.strip.tr(',', '')}%" - where(['LOWER(login) LIKE :s OR ' + - "LOWER(#{firstnamelastname}) LIKE :s OR " + - "LOWER(#{lastnamefirstname}) LIKE :s OR " + - 'LOWER(mail) LIKE :s', - { s: }]) + sql = <<~SQL + LOWER(login) LIKE :s + OR unaccent(LOWER(#{firstnamelastname})) LIKE unaccent(:s) + OR unaccent(LOWER(#{lastnamefirstname})) LIKE unaccent(:s) + OR LOWER(mail) LIKE :s + SQL + + where([sql, { s: }]) .order(:type, :login, :lastname, :firstname, :mail) end end diff --git a/app/models/queries/filters/shared/user_name_filter.rb b/app/models/queries/filters/shared/user_name_filter.rb index 88004837ea56..aa501bdebb50 100644 --- a/app/models/queries/filters/shared/user_name_filter.rb +++ b/app/models/queries/filters/shared/user_name_filter.rb @@ -44,13 +44,13 @@ def self.key def where case operator when '=' - ["#{sql_concat_name} IN (?)", sql_value] + ["#{sql_concat_name} IN (:s) OR unaccent(#{sql_concat_name}) IN (:s)", { s: sql_value }] when '!' - ["#{sql_concat_name} NOT IN (?)", sql_value] + ["#{sql_concat_name} NOT IN (:s) AND unaccent(#{sql_concat_name}) NOT IN (:s)", { s: sql_value }] when '~', '**' - ["#{sql_concat_name} LIKE ?", "%#{sql_value}%"] + ["unaccent(#{sql_concat_name}) LIKE unaccent(:s)", { s: "%#{sql_value}%" }] when '!~' - ["#{sql_concat_name} NOT LIKE ?", "%#{sql_value}%"] + ["unaccent(#{sql_concat_name}) NOT LIKE :s", { s: "%#{sql_value}%" }] end end @@ -70,7 +70,7 @@ def sql_concat_name when :firstname_lastname "LOWER(CONCAT(users.firstname, CONCAT(' ', users.lastname)))" when :firstname - 'LOWER(users.firstname)' + 'LOWER(users.firstname))' when :lastname_firstname, :lastname_coma_firstname "LOWER(CONCAT(users.lastname, CONCAT(' ', users.firstname)))" when :lastname_n_firstname diff --git a/db/migrate/20240311111957_enable_unaccent_extension.rb b/db/migrate/20240311111957_enable_unaccent_extension.rb new file mode 100644 index 000000000000..7f376ffa71e6 --- /dev/null +++ b/db/migrate/20240311111957_enable_unaccent_extension.rb @@ -0,0 +1,48 @@ + #-- copyright + # OpenProject is an open source project management software. + # Copyright (C) 2012-2024 the OpenProject GmbH + # + # This program is free software; you can redistribute it and/or + # modify it under the terms of the GNU General Public License version 3. + # + # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: + # Copyright (C) 2006-2013 Jean-Philippe Lang + # Copyright (C) 2010-2013 the ChiliProject Team + # + # This program is free software; you can redistribute it and/or + # modify it under the terms of the GNU General Public License + # as published by the Free Software Foundation; either version 2 + # of the License, or (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program; if not, write to the Free Software + # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + # + # See COPYRIGHT and LICENSE files for more details. + #++ + + class EnableUnaccentExtension < ActiveRecord::Migration[7.1] + def up + ActiveRecord::Base.connection.execute("CREATE EXTENSION IF NOT EXISTS unaccent WITH SCHEMA pg_catalog;") + rescue StandardError => e + raise unless e.message.include?('unaccent') + + raise <<~MESSAGE + \e[33mWARNING:\e[0m Could not find or enable the `unaccent` extension for PostgreSQL. + In order to benefit from this performance improvement, please install the postgresql-contrib module + for your PostgreSQL installation and re-run this migration. + + Read more about the contrib module at `https://www.postgresql.org/docs/current/contrib.html` . + To re-run this migration use the following command `bin/rails db:migrate:redo VERSION=20230328154645` + MESSAGE + end + + def down + ActiveRecord::Base.connection.execute("DROP EXTENSION IF EXISTS unaccent CASCADE;") + end + end diff --git a/spec/features/users/unaccent_user_filter_spec.rb b/spec/features/users/unaccent_user_filter_spec.rb new file mode 100644 index 000000000000..edbd3cde9213 --- /dev/null +++ b/spec/features/users/unaccent_user_filter_spec.rb @@ -0,0 +1,89 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require_relative '../principals/shared_memberships_examples' + +RSpec.describe 'Finding users with accents', :js, :with_cuprite do + shared_let(:project) { create(:project) } + shared_let(:principal) { create(:user, firstname: 'Cécile', lastname: 'Foobar') } + shared_let(:admin) { create(:admin) } + shared_let(:work_package) { create(:work_package, project:) } + shared_let(:role) do + create(:project_role, + name: 'Developer', + permissions: %i[view_work_packages edit_work_packages work_package_assigned]) + end + + let(:members_page) { Pages::Members.new project.identifier } + let(:wp_page) { Pages::FullWorkPackage.new work_package } + let(:assignee_field) { wp_page.edit_field :assignee } + + current_user { admin } + + it 'finds a user with accents in the name in the global administration' do + visit users_path + + fill_in 'name', with: 'Cecile' + click_on 'Apply' + expect(page).to have_current_path /name=Cecile/ + expect(page).to have_css('td.firstname', text: 'Cécile') + + fill_in 'name', with: 'Cécile' + click_on 'Apply' + expect(page).to have_current_path /name=C%C3%A9cile/ + expect(page).to have_css('td.firstname', text: 'Cécile') + end + + it 'can add the user as member and assignee' do + visit project_members_path(project) + + members_page.open_new_member! + members_page.search_and_select_principal! 'Cecile', + 'Cécile Foobar' + members_page.select_role! 'Developer' + + click_on 'Add' + expect(members_page).to have_added_user 'Cécile Foobar' + + members_page.open_filters! + members_page.search_for_name 'Cecile' + members_page.find_user 'Cécile Foobar' + + visit project_work_package_path(project, work_package) + assignee_field.activate! + + assignee_field.openSelectField + assignee_field.autocomplete('Cecile', select_text: 'Cécile Foobar', select: true) + wait_for_network_idle + + wp_page.expect_and_dismiss_toaster message: 'Successful update.' + assignee_field.expect_inactive! + assignee_field.expect_state_text 'Cécile Foobar' + end +end diff --git a/spec/support/edit_fields/edit_field.rb b/spec/support/edit_fields/edit_field.rb index e159972abac6..f859443768b1 100644 --- a/spec/support/edit_fields/edit_field.rb +++ b/spec/support/edit_fields/edit_field.rb @@ -183,11 +183,11 @@ def set_value(content) end end - def autocomplete(query, select: true) + def autocomplete(query, select: true, select_text: query) raise ArgumentError.new('Is not an autocompleter field') unless autocompleter_field? if select - select_autocomplete field_container, query:, results_selector: 'body' + select_autocomplete field_container, query:, select_text:, results_selector: 'body' else search_autocomplete field_container, query:, results_selector: 'body' end