Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix nicknames uniqueness with different cases #8792

Merged
merged 42 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4a04fa2
allowing only lowercase in the nickname
eliegaboriau Feb 1, 2022
71d1d48
nickname test system
eliegaboriau Feb 1, 2022
fbd9a19
add in locales the "lowercase" precision
eliegaboriau Feb 1, 2022
cef69cd
changing routing for timeline
eliegaboriau Feb 2, 2022
2894a3d
changing routing for activities
eliegaboriau Feb 2, 2022
cd2ed9f
changing routing for other profile tabs
eliegaboriau Feb 2, 2022
5d294e8
migration to change commune nicknames
eliegaboriau Feb 4, 2022
7c4a9d9
linter
eliegaboriau Feb 7, 2022
ed504ed
minor changes
eliegaboriau Feb 7, 2022
9d7a1a3
Refactor migration for lisibility
eliegaboriau Feb 8, 2022
5246429
Merge branch 'decidim:develop' into fix/nickname_uniqueness-6092
eliegaboriau Feb 8, 2022
360e912
Merge branch 'decidim:develop' into fix/nickname_uniqueness-6092
eliegaboriau Feb 8, 2022
a20f518
add fix into changelog
eliegaboriau Feb 8, 2022
44e0533
linter
eliegaboriau Feb 8, 2022
8a63ec6
Update CHANGELOG.md
eliegaboriau Feb 8, 2022
a12fe6b
add notification when updating nickname
eliegaboriau Feb 9, 2022
0df79b9
fix number of notification
eliegaboriau Feb 9, 2022
9a66796
Add notification when updating nickname (#3)
eliegaboriau Feb 11, 2022
539f837
Merge branch 'develop' into fix/nickname_uniqueness-6092
eliegaboriau Feb 17, 2022
ea339fc
update message in notification
eliegaboriau Feb 17, 2022
f266a20
Merge branch 'fix/nickname_uniqueness-6092' of https://github.com/eli…
eliegaboriau Feb 17, 2022
159abf1
update test
eliegaboriau Feb 17, 2022
548b292
minor changes
eliegaboriau Feb 17, 2022
ed4835f
Change requests nickname (#8)
eliegaboriau Mar 9, 2022
721092f
add test notification
eliegaboriau Mar 9, 2022
9b26951
linter
eliegaboriau Mar 9, 2022
81d49a7
Update CHANGELOG.md
eliegaboriau Mar 9, 2022
766c861
Update CHANGELOG.md
eliegaboriau Mar 9, 2022
360a73e
Update CHANGELOG.md
eliegaboriau Mar 11, 2022
5f0f220
Update CHANGELOG.md
eliegaboriau Mar 21, 2022
af7f9d4
Update CHANGELOG.md
eliegaboriau Mar 21, 2022
99f2bae
Update CHANGELOG.md
eliegaboriau Mar 21, 2022
33d9f81
Update account_form.rb
eliegaboriau Mar 21, 2022
56a54be
linter
eliegaboriau Mar 21, 2022
caf4f91
remove the if statement
eliegaboriau Apr 25, 2022
10b456f
remove test task
eliegaboriau Apr 25, 2022
b52970f
relaunch tests
eliegaboriau Apr 26, 2022
7bea466
add organization criteria
eliegaboriau May 2, 2022
034b231
lint
eliegaboriau May 2, 2022
0026fb6
use nicknamize instead of random numbers
eliegaboriau May 2, 2022
44960e5
add the not deleted scope
eliegaboriau May 2, 2022
1ce8f45
update nicknamizable to check for case
eliegaboriau May 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ PR [\#8645](https://github.com/decidim/decidim/pull/8645) we now only allow PNG
- **decidim-debates**, **decidim-initiatives**, **decidim-meetings**: The resource search classes `Decidim::Debates::DebateSearch`, `Decidim::Intitatives::InitiativeSearch` and `Decidim::Meetings::MeetingSearch` are rewritten for the Ransack searches due to Searchlight removal at [\#8748](https://github.com/decidim/decidim/pull/8748). The role of these classes is now to pass contextual information to the searches, such as the current user. All other search filtering should happen directly through Ransack.
- **decidim-meetings**: The `visible_meetings_for` scope for the `Meeting` model has been renamed to `visible_for` in [\#8748](https://github.com/decidim/decidim/pull/8748) for consistency.
- **decidim-core**: The `official_origin`, `participants_origin`, `user_group_origin` and `meeting_origin` scopes for the `Decidim::Authorable` and `Decidim::Coauthorable` concerns have been changed to `with_official_origin`, `with_participants_origin`, `with_user_group_origin` and `with_meeting_origin` respectively in [\#8748](https://github.com/decidim/decidim/pull/8748) for consistency. See the Searchlight removal change notes for reasoning.
- **decidim-core**: Nicknames are now differents case insensitively, a rake task has been created to check every nickname and modify them if some are similar (Launch it with "bundle exec rake decidim:upgrade:fix_nickname_uniqueness"). Routing and mentions has been made case insensitive for every tab in profiles.

#### Searchlight removal causes changes in the participant searches

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def ensure_profile_holder
def profile_holder
return if params[:nickname].blank?

@profile_holder ||= Decidim::UserBaseEntity.find_by(nickname: params[:nickname], organization: current_organization)
@profile_holder ||= Decidim::UserBaseEntity.find_by("LOWER(nickname) = ? AND decidim_organization_id = ?", params[:nickname].downcase, current_organization.id)
andreslucena marked this conversation as resolved.
Show resolved Hide resolved
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ def index
private

def user
@user ||= current_organization.users.find_by(nickname: params[:nickname])
return unless params[:nickname]

@user ||= current_organization.users.find_by("LOWER(nickname) = ?", params[:nickname].downcase)
andreslucena marked this conversation as resolved.
Show resolved Hide resolved
end

def activities
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ def index
private

def user
@user ||= Decidim::User.find_by(
organization: current_organization,
nickname: params[:nickname]
)
return unless params[:nickname]

@user ||= current_organization.users.find_by("LOWER(nickname) = ?", params[:nickname].downcase)
andreslucena marked this conversation as resolved.
Show resolved Hide resolved
end

def activities
Expand Down
21 changes: 21 additions & 0 deletions decidim-core/app/events/decidim/change_nickname_event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen-string_literal: true

module Decidim
class ChangeNicknameEvent < Decidim::Events::SimpleEvent
include Decidim::Events::NotificationEvent
delegate :organization, to: :user, prefix: false
delegate :url_helpers, to: "Decidim::Core::Engine.routes"

i18n_attributes :link_to_account_settings

def notification_title
I18n.t("decidim.events.nickname_event.notification_body", i18n_options).to_s.html_safe
end

def i18n_options
{
link_to_account_settings: url_helpers.account_path
}
end
end
end
5 changes: 3 additions & 2 deletions decidim-core/app/forms/decidim/account_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ def unique_email

def unique_nickname
return true if Decidim::User.where(
organization: context.current_organization,
nickname: nickname
"decidim_organization_id = ? AND LOWER(nickname) = ? ",
context.current_organization.id,
nickname.downcase
).where.not(id: context.current_user.id).empty?

errors.add :nickname, :taken
Expand Down
6 changes: 4 additions & 2 deletions decidim-core/app/forms/decidim/registration_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class RegistrationForm < Form
attribute :current_locale, String

validates :name, presence: true
validates :nickname, presence: true, format: /\A[\w\-]+\z/, length: { maximum: Decidim::User.nickname_max_length }
validates :nickname, presence: true, format: Decidim::User::REGEXP_NICKNAME, length: { maximum: Decidim::User.nickname_max_length }
eliegaboriau marked this conversation as resolved.
Show resolved Hide resolved
validates :email, presence: true, 'valid_email_2/email': { disposable: true }
validates :password, confirmation: true
validates :password, password: { name: :name, email: :email, username: :nickname }
Expand All @@ -39,7 +39,9 @@ def email_unique_in_organization
end

def nickname_unique_in_organization
errors.add :nickname, :taken if User.no_active_invitation.find_by(nickname: nickname, organization: current_organization).present?
return false unless nickname

errors.add :nickname, :taken if User.no_active_invitation.find_by("LOWER(nickname)= ? AND decidim_organization_id = ?", nickname.downcase, current_organization.id).present?
end

def no_pending_invitations_exist
Expand Down
4 changes: 3 additions & 1 deletion decidim-core/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,8 @@ en:
email_outro: You have received this notification because you are an admin of the platform.
email_subject: A user group has updated its profile
notification_title: The %{user_group_name} user group has updated its profile, leaving it unverified. You can now verify it in the <a href="%{groups_admin_path}">admin panel</a>.
nickname_event:
notification_body: We have corrected the way nicknames are used so that there are no duplicates, and that's why we have removed the case sensitive rule. <br/> Your nickname was created after another one with the same name, so we have automatically renamed it. You can change it from <a href="%{link_to_account_settings}">your account settings</a>.
notification_event:
notification_title: An event occured to <a href="%{resource_path}">%{resource_title}</a>.
reports:
Expand Down Expand Up @@ -1462,7 +1464,7 @@ en:
invitations:
edit:
header: Finish creating your account
nickname_help: Your nickname in %{organization}.
nickname_help: Your nickname in %{organization}. Can only contain letters, numbers, '-' and '_'.
submit_button: Save
subtitle: If you accept the invitation please set your nickname and password.
invitation_removed: Your invitation was removed.
Expand Down
6 changes: 3 additions & 3 deletions decidim-core/lib/decidim/content_parsers/user_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class UserParser < BaseParser
# @return [String] the content with the valid mentions replaced by a global id
def rewrite
content.gsub(MENTION_REGEX) do |match|
users[match[1..-1]]&.to_global_id&.to_s || match
users[match[1..-1].downcase]&.to_global_id&.to_s || match
end
end

Expand All @@ -43,11 +43,11 @@ def users
end

def existing_users
@existing_users ||= Decidim::User.where(organization: current_organization, nickname: content_nicknames)
@existing_users ||= Decidim::User.where("decidim_organization_id = ? AND LOWER(nickname) IN (?)", current_organization.id, content_nicknames)
end

def content_nicknames
@content_nicknames ||= content.scan(MENTION_REGEX).flatten.uniq
@content_nicknames ||= content.scan(MENTION_REGEX).flatten.uniq.map!(&:downcase)
end

def current_organization
Expand Down
2 changes: 1 addition & 1 deletion decidim-core/lib/decidim/nicknamizable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def disambiguate(name, scope)
candidate = name

2.step do |n|
return candidate unless exists?(scope.merge(nickname: candidate))
return candidate if Decidim::User.where("nickname ILIKE ?", candidate.downcase).where(scope).empty?

candidate = numbered_variation_of(name, n)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

namespace :decidim do
namespace :upgrade do
desc "Modify nicknames with random numbers when exists similar ones case insensitively"
task fix_nickname_uniqueness: :environment do
logger = Logger.new($stdout)
logger.info("Updating conflicting user nicknames...")

# list of users already changed in the process
has_changed = []

Decidim::User.not_deleted.find_each do |user|
next if has_changed.include? user.id

Decidim::User.where(organization: user.organization)
.where("nickname ILIKE ?", user.nickname.downcase)
.where.not(id: has_changed + [user.id])
eliegaboriau marked this conversation as resolved.
Show resolved Hide resolved
.not_deleted
.order(:created_at)
.each do |similar_user|
# change her nickname to the lowercased one with numbers if needed
begin
update_user_nickname(similar_user, Decidim::UserBaseEntity.nicknamize(similar_user.nickname, organization: similar_user.organization))
rescue ActiveRecord::RecordInvalid => e
logger.warn("User ID (#{similar_user.id}) : #{e}")
end
has_changed.append(similar_user.id)
end
end
logger.info("Process terminated, #{has_changed.count} users nickname have been updated.")
end

private

def send_notification_to(user)
Decidim::EventsManager.publish({
event: "decidim.events.nickname_event",
event_class: Decidim::ChangeNicknameEvent,
affected_users: [user],
resource: user
})
end

def update_user_nickname(user, new_nickname)
user.update!(nickname: new_nickname)
send_notification_to(user)
user
end
end
end
13 changes: 13 additions & 0 deletions decidim-core/spec/content_parsers/decidim/user_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,18 @@ module Decidim
expect(parser.metadata.users).to eq([])
end
end

context "when mentioning a user with a wrong case" do
let(:content) { "This text mentions a user with wrong case : @#{user.nickname.upcase}" }

it "rewrite the good user" do
expect(parser.rewrite).to eq("This text mentions a user with wrong case : #{user.to_global_id}")
end

it "returns the correct metadata" do
expect(parser.metadata).to be_a(Decidim::ContentParsers::UserParser::Metadata)
expect(parser.metadata.users).to eq([user])
end
end
end
end
25 changes: 25 additions & 0 deletions decidim-core/spec/controllers/decidim/profiles_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

require "spec_helper"

module Decidim
describe ProfilesController, type: :controller do
routes { Decidim::Core::Engine.routes }

let(:organization) { create(:organization) }
let!(:user) { create(:user, nickname: "Nick", organization: organization) }

before do
request.env["decidim.current_organization"] = organization
end

describe "#badges" do
context "with an user with uppercase" do
it "returns the lowercased user" do
get :badges, params: { nickname: "NICK" }
expect(response).to render_template(:show)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Decidim
routes { Decidim::Core::Engine.routes }

let(:organization) { create(:organization) }
let!(:user) { create(:user, nickname: "Nick", organization: organization) }

before do
request.env["decidim.current_organization"] = organization
Expand All @@ -20,6 +21,13 @@ module Decidim
end.to raise_error(ActionController::RoutingError, "Missing user: foobar")
end
end

context "with an user with uppercase" do
it "returns the lowercased user" do
get :index, params: { nickname: "NICK" }
expect(response).to render_template(:index)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require "spec_helper"

module Decidim
describe UserTimelineController, type: :controller do
routes { Decidim::Core::Engine.routes }

let(:organization) { create(:organization) }
let!(:user) { create(:user, :confirmed, nickname: "Nick", organization: organization) }

before do
request.env["decidim.current_organization"] = organization
sign_in user
end

describe "#index" do
context "with a different user than me" do
it "raises an ActionController::RoutingError" do
expect do
get :index, params: { nickname: "foobar" }
end.to raise_error(ActionController::RoutingError, "Not Found")
end
end

context "with my user with uppercase" do
it "returns the lowercased user" do
get :index, params: { nickname: "NICK" }
expect(response).to render_template(:index)
end
end
end
end
end
17 changes: 17 additions & 0 deletions decidim-core/spec/events/decidim/change_nickname_event_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require "spec_helper"

describe Decidim::ChangeNicknameEvent do
include_context "when a simple event"

let(:event_name) { "decidim.events.change_nickname_event" }
let(:resource) { create :user }
let(:author) { resource }

describe "notification_title" do
it "is generated correctly" do
expect(subject.notification_title).to include("We have corrected the way nicknames are used so that there are no duplicates, and that's why we have removed the case sensitive rule. <br/> Your nickname was created after another one with the same name, so we have automatically renamed it. You can change it from <a href=\"/account\">your account settings</a>.")
end
end
end
2 changes: 1 addition & 1 deletion decidim-core/spec/forms/registration_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ module Decidim
end

context "when the nickname already exists" do
let!(:user) { create(:user, organization: organization, nickname: nickname) }
let!(:user) { create(:user, organization: organization, nickname: nickname.upcase) }

it { is_expected.to be_invalid }

Expand Down
21 changes: 21 additions & 0 deletions decidim-core/spec/system/authentication_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,27 @@
end
end

context "when nickname is not unique case insensitively" do
let!(:user) { create(:user, nickname: "Nick", organization: organization) }

it "show an error message" do
find(".sign-up-link").click

within ".new_user" do
fill_in :registration_user_email, with: "user@example.org"
fill_in :registration_user_name, with: "Responsible Citizen"
fill_in :registration_user_nickname, with: "NiCk"
fill_in :registration_user_password, with: "DfyvHn425mYAy2HL"
fill_in :registration_user_password_confirmation, with: "DfyvHn425mYAy2HL"
check :registration_user_tos_agreement
check :registration_user_newsletter
find("*[type=submit]").click
end

expect(page).to have_content("has already been taken")
end
end

context "when sign up is disabled" do
let(:organization) { create(:organization, users_registration_mode: :existing) }

Expand Down