]> git.openstreetmap.org Git - rails.git/commitdiff
Merge remote-tracking branch 'upstream/pull/4482'
authorTom Hughes <tom@compton.nu>
Wed, 17 Jan 2024 18:32:08 +0000 (18:32 +0000)
committerTom Hughes <tom@compton.nu>
Wed, 17 Jan 2024 18:32:08 +0000 (18:32 +0000)
.rubocop.yml
Gemfile.lock
app/models/user.rb
app/validators/normalized_uniqueness_validator.rb [new file with mode: 0644]
db/migrate/20231213182102_add_canonical_user_index.rb [new file with mode: 0644]
db/structure.sql
test/models/user_test.rb

index 1e18afd83581ba0fc47d728032b3ade65f091b1a..ddfb63cae0610942a1ac6f107267e2c283ea2b50 100644 (file)
@@ -68,6 +68,9 @@ Rails/SkipsModelValidations:
     - 'db/migrate/*.rb'
     - 'app/controllers/users_controller.rb'
 
+Style/ArgumentsForwarding:
+  Enabled: false
+
 Style/Documentation:
   Enabled: false
 
index 0135ebb875b59c9010a516fe72e3e92b5300a160..96dea6362560096f278c0370500ec878b88bd606 100644 (file)
@@ -96,8 +96,8 @@ GEM
     autoprefixer-rails (10.4.16.0)
       execjs (~> 2)
     aws-eventstream (1.3.0)
-    aws-partitions (1.877.0)
-    aws-sdk-core (3.190.1)
+    aws-partitions (1.880.0)
+    aws-sdk-core (3.190.2)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.8)
@@ -126,7 +126,7 @@ GEM
     bigdecimal (3.1.5)
     binding_of_caller (1.0.0)
       debug_inspector (>= 0.0.1)
-    bootsnap (1.17.0)
+    bootsnap (1.17.1)
       msgpack (~> 1.2)
     bootstrap (5.3.2)
       autoprefixer-rails (>= 9.1.0)
@@ -153,7 +153,7 @@ GEM
       rack-test (>= 0.6.3)
       regexp_parser (>= 1.5, < 3.0)
       xpath (~> 3.2)
-    concurrent-ruby (1.2.2)
+    concurrent-ruby (1.2.3)
     config (5.1.0)
       deep_merge (~> 1.2, >= 1.2.1)
       dry-validation (~> 1.0, >= 1.0.0)
@@ -289,7 +289,7 @@ GEM
     image_processing (1.12.2)
       mini_magick (>= 4.9.5, < 5)
       ruby-vips (>= 2.0.17, < 3)
-    image_size (3.3.0)
+    image_size (3.4.0)
     in_threads (1.6.0)
     io-console (0.7.1)
     irb (1.11.1)
@@ -333,7 +333,7 @@ GEM
     mini_portile2 (2.8.5)
     mini_racer (0.8.0)
       libv8-node (~> 18.16.0.0)
-    minitest (5.20.0)
+    minitest (5.21.1)
     msgpack (1.7.2)
     multi_json (1.15.0)
     multi_xml (0.6.0)
@@ -401,7 +401,7 @@ GEM
       omniauth (~> 2.0)
     openstreetmap-deadlock_retry (1.3.1)
     parallel (1.24.0)
-    parser (3.3.0.2)
+    parser (3.3.0.4)
       ast (~> 2.4.1)
       racc
     pg (1.5.4)
@@ -485,11 +485,11 @@ GEM
     rouge (4.2.0)
     rtlcss (0.2.1)
       mini_racer (>= 0.6.3)
-    rubocop (1.59.0)
+    rubocop (1.60.0)
       json (~> 2.3)
       language_server-protocol (>= 3.17.0)
       parallel (~> 1.10)
-      parser (>= 3.2.2.4)
+      parser (>= 3.3.0.2)
       rainbow (>= 2.2.2, < 4.0)
       regexp_parser (>= 1.8, < 3.0)
       rexml (>= 3.2.5, < 4.0)
index bc95f20c1568fe70cdd436c3ce96d1b8fd48ad42..b04f8e2b914c274babd1d9aec55c5f85ce4705e7 100644 (file)
 #
 # Indexes
 #
-#  users_auth_idx                (auth_provider,auth_uid) UNIQUE
-#  users_display_name_idx        (display_name) UNIQUE
-#  users_display_name_lower_idx  (lower((display_name)::text))
-#  users_email_idx               (email) UNIQUE
-#  users_email_lower_idx         (lower((email)::text))
-#  users_home_idx                (home_tile)
+#  users_auth_idx                    (auth_provider,auth_uid) UNIQUE
+#  users_display_name_canonical_idx  (lower(NORMALIZE(display_name, NFKC)))
+#  users_display_name_idx            (display_name) UNIQUE
+#  users_display_name_lower_idx      (lower((display_name)::text))
+#  users_email_idx                   (email) UNIQUE
+#  users_email_lower_idx             (lower((email)::text))
+#  users_home_idx                    (home_tile)
 #
 
 class User < ApplicationRecord
@@ -95,7 +96,7 @@ class User < ApplicationRecord
   validates :display_name, :presence => true, :length => 3..255,
                            :exclusion => %w[new terms save confirm confirm-email go_public reset-password forgot-password suspended]
   validates :display_name, :if => proc { |u| u.display_name_changed? },
-                           :uniqueness => { :case_sensitive => false }
+                           :normalized_uniqueness => { :case_sensitive => false }
   validates :display_name, :if => proc { |u| u.display_name_changed? },
                            :characters => { :url_safe => true },
                            :whitespace => { :leading => false, :trailing => false }
@@ -132,7 +133,7 @@ class User < ApplicationRecord
       user = find_by("email = ? OR display_name = ?", options[:username].strip, options[:username])
 
       if user.nil?
-        users = where("LOWER(email) = LOWER(?) OR LOWER(display_name) = LOWER(?)", options[:username].strip, options[:username])
+        users = where("LOWER(email) = LOWER(?) OR LOWER(NORMALIZE(display_name, NFKC)) = LOWER(NORMALIZE(?, NFKC))", options[:username].strip, options[:username])
 
         user = users.first if users.count == 1
       end
diff --git a/app/validators/normalized_uniqueness_validator.rb b/app/validators/normalized_uniqueness_validator.rb
new file mode 100644 (file)
index 0000000..eb3600c
--- /dev/null
@@ -0,0 +1,18 @@
+class NormalizedUniquenessValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    relation = if options.fetch(:case_sensitive, true)
+                 record.class.where("NORMALIZE(#{attribute}, NFKC) = NORMALIZE(?, NFKC)", value)
+               else
+                 record.class.where("LOWER(NORMALIZE(#{attribute}, NFKC)) = LOWER(NORMALIZE(?, NFKC))", value)
+               end
+
+    relation = relation.where.not(record.class.primary_key => [record.id_in_database]) if record.persisted?
+
+    if relation.exists?
+      error_options = options.except(:case_sensitive)
+      error_options[:value] = value
+
+      record.errors.add(attribute, :taken, **error_options)
+    end
+  end
+end
diff --git a/db/migrate/20231213182102_add_canonical_user_index.rb b/db/migrate/20231213182102_add_canonical_user_index.rb
new file mode 100644 (file)
index 0000000..905fb32
--- /dev/null
@@ -0,0 +1,7 @@
+class AddCanonicalUserIndex < ActiveRecord::Migration[7.1]
+  disable_ddl_transaction!
+
+  def change
+    add_index :users, "LOWER(NORMALIZE(display_name, NFKC))", :name => "users_display_name_canonical_idx", :algorithm => :concurrently
+  end
+end
index 0563417cdf07f81577a51a8dab9770d8b57ea423..c9fdebbc8a9fa5253d9341fdadf2caada58cc1d8 100644 (file)
@@ -2922,6 +2922,13 @@ CREATE INDEX user_tokens_user_id_idx ON public.user_tokens USING btree (user_id)
 CREATE UNIQUE INDEX users_auth_idx ON public.users USING btree (auth_provider, auth_uid);
 
 
+--
+-- Name: users_display_name_canonical_idx; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX users_display_name_canonical_idx ON public.users USING btree (lower(NORMALIZE(display_name, NFKC)));
+
+
 --
 -- Name: users_display_name_idx; Type: INDEX; Schema: public; Owner: -
 --
@@ -3581,6 +3588,7 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('23'),
 ('22'),
 ('21'),
+('20231213182102'),
 ('20231206141457'),
 ('20231117170422'),
 ('20231101222146'),
index 4e2675a2e23b4bfc2882ca6cccde5c7df82d762c..d21512f2a1861d317c13664f5ebcbc7ef17d3f39 100644 (file)
@@ -27,10 +27,13 @@ class UserTest < ActiveSupport::TestCase
   end
 
   def test_unique_display_name
-    existing_user = create(:user)
-    new_user = build(:user, :display_name => existing_user.display_name)
-    assert_not new_user.save
-    assert_includes new_user.errors[:display_name], "has already been taken"
+    create(:user, :display_name => "H\u{e9}nryIV")
+
+    %W[H\u{e9}nryIV he\u{301}nryiv H\u{c9}nry\u2163 he\u{301}nry\u2173].each do |name|
+      new_user = build(:user, :display_name => name)
+      assert_not new_user.save
+      assert_includes new_user.errors[:display_name], "has already been taken"
+    end
   end
 
   def test_email_valid