diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 539e92eeca457d5fd7f47ae289a572ffd13ffd52..f136a8c3a088e322af3c9c0762d15df9e42eb9fb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -634,9 +634,10 @@ $status-icon-size: 22px; /* * Social Icons */ -$twitter: #1d9bf0; -$skype: #0078d7; +$discord: #5865f2; $linkedin: #2867b2; +$skype: #0078d7; +$twitter: #1d9bf0; /* * Award emoji diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss index ac1e9fb024b93abf08c8c3a7da8bafc955593e87..fc745433f1befc7d9bf1f5363c66148a56ce6a4d 100644 --- a/app/assets/stylesheets/page_bundles/profile.scss +++ b/app/assets/stylesheets/page_bundles/profile.scss @@ -240,6 +240,10 @@ color: $twitter; } +.discord-icon { + color: $discord; +} + .key-created-at { line-height: 42px; } diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 4f379d8a75bffeacddbe28751786c6d322bc3aaa..23ec80c1fa92fb97e318597e39dc11e337fb3c5b 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -359,6 +359,7 @@ def allowed_user_params :skype, :theme_id, :twitter, + :discord, :username, :website_url, :note, diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 93b131369b8d1e3bbc272f7296155d129b2f8d8d..45b274fc9201e2115bb17ab8a62a2e89c2e21e49 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -127,6 +127,7 @@ def user_params_attributes :commit_email, :skype, :twitter, + :discord, :username, :website_url, :organization, diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ef174584a4bd48f4d9b0cca2f2ec20c8259ecbbf..4afbe98226e3417bcd245fb9eae11a3fa5a82c9b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -368,6 +368,12 @@ def twitter_url(user) end end + def discord_url(user) + return '' if user.discord.blank? + + "https://discord.com/users/#{user.discord}" + end + def collapsed_sidebar? cookies["sidebar_collapsed"] == "true" end diff --git a/app/models/user.rb b/app/models/user.rb index 535f1bd874aba87893e91c91c410fe498d61963e..6151ba54555df777f7ea0715abbc31b7fd065520 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -380,6 +380,7 @@ def update_tracked_fields!(request) delegate :website_url, :website_url=, to: :user_detail, allow_nil: true delegate :location, :location=, to: :user_detail, allow_nil: true delegate :organization, :organization=, to: :user_detail, allow_nil: true + delegate :discord, :discord=, to: :user_detail, allow_nil: true accepts_nested_attributes_for :user_preference, update_only: true accepts_nested_attributes_for :user_detail, update_only: true diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb index b6765cb02854a8d9453513aae65dec32cd505995..9d3df3d6400a93bd1426813a46a22f51efcee8b0 100644 --- a/app/models/user_detail.rb +++ b/app/models/user_detail.rb @@ -14,11 +14,13 @@ class UserDetail < ApplicationRecord DEFAULT_FIELD_LENGTH = 500 + validates :discord, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true + validate :discord_format validates :linkedin, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true - validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true - validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true + validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true + validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true, if: :website_url_changed? before_validation :sanitize_attrs @@ -27,7 +29,7 @@ class UserDetail < ApplicationRecord enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true def sanitize_attrs - %i[linkedin skype twitter website_url].each do |attr| + %i[discord linkedin skype twitter website_url].each do |attr| value = self[attr] self[attr] = Sanitize.clean(value) if value.present? end @@ -41,13 +43,20 @@ def sanitize_attrs def prevent_nil_fields self.bio = '' if bio.nil? + self.discord = '' if discord.nil? self.linkedin = '' if linkedin.nil? - self.twitter = '' if twitter.nil? - self.skype = '' if skype.nil? self.location = '' if location.nil? self.organization = '' if organization.nil? + self.skype = '' if skype.nil? + self.twitter = '' if twitter.nil? self.website_url = '' if website_url.nil? end end +def discord_format + return if discord.blank? || discord =~ %r{\A\d{17,20}\z} + + errors.add(:discord, _('must contain only a discord user ID.')) +end + UserDetail.prepend_mod_with('UserDetail') diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index a140d78018062d454e8b208fc713b8d44bb643f6..b2838473ee6bdc000c58fea93003d767f4fdaf7a 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -103,6 +103,15 @@ .form-group.gl-form-group = f.label :twitter = f.text_field :twitter, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|@username") + .form-group.gl-form-group + - external_accounts_help_url = help_page_path('user/profile/index', anchor: 'add-external-accounts-to-your-user-profile-page') + - external_accounts_link_start = ''.html_safe % { url: external_accounts_help_url } + - external_accounts_docs_link = s_('Profiles|Your Discord user ID. Should be between %{min} and %{max} digits long. %{external_accounts_link_start}Learn more.%{external_accounts_link_end}').html_safe % { min: '17', max: '20', external_accounts_link_start: external_accounts_link_start, external_accounts_link_end: ''.html_safe } + = f.label :discord + = f.text_field :discord, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|User ID") + %small.form-text.text-gl-muted + = external_accounts_docs_link + .form-group.gl-form-group = f.label :website_url, s_('Profiles|Website url') = f.text_field :website_url, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|https://website.com") diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index af29de6b0c4189a1f0b1fafad10cde2d63af280a..3b9884526fd94178d13d1da5dfff6611b43a9baf 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -101,6 +101,10 @@ = render 'middle_dot_divider', breakpoint: 'sm' do = link_to twitter_url(@user), class: 'gl-hover-text-decoration-none', title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do = sprite_icon('twitter', css_class: 'twitter-icon') + - unless @user.discord.blank? + = render 'middle_dot_divider', breakpoint: 'sm' do + = link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do + = sprite_icon('discord', css_class: 'discord-icon') - unless @user.website_url.blank? = render 'middle_dot_divider', stacking: true do - if Feature.enabled?(:security_auto_fix) && @user.bot? diff --git a/db/migrate/20221128155738_add_discord_to_user_details.rb b/db/migrate/20221128155738_add_discord_to_user_details.rb new file mode 100644 index 0000000000000000000000000000000000000000..4d59a53dcd7ce2ae67baaee703c3fbe21d53a892 --- /dev/null +++ b/db/migrate/20221128155738_add_discord_to_user_details.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddDiscordToUserDetails < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + # rubocop:disable Migration/AddLimitToTextColumns + # limits are added in 20221128165833_add_discord_field_limit_to_user_details.rb + def change + add_column :user_details, :discord, :text, default: '', null: false + end + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20221128165833_add_discord_field_limit_to_user_details.rb b/db/migrate/20221128165833_add_discord_field_limit_to_user_details.rb new file mode 100644 index 0000000000000000000000000000000000000000..a63b2019b200d8fa1b302aeb307ad75bf2f70c16 --- /dev/null +++ b/db/migrate/20221128165833_add_discord_field_limit_to_user_details.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddDiscordFieldLimitToUserDetails < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + USER_DETAILS_FIELD_LIMIT = 500 + + def up + add_text_limit :user_details, :discord, USER_DETAILS_FIELD_LIMIT + end + + def down + remove_text_limit :user_details, :discord + end +end diff --git a/db/schema_migrations/20221128155738 b/db/schema_migrations/20221128155738 new file mode 100644 index 0000000000000000000000000000000000000000..5322aa1f75e5cedfdadabbc95fbd4929a306bd19 --- /dev/null +++ b/db/schema_migrations/20221128155738 @@ -0,0 +1 @@ +39ca72ad461ff7b56ce6feed351ef46ee9f3584a8c3c9383ca75f44b61baa1a1 \ No newline at end of file diff --git a/db/schema_migrations/20221128165833 b/db/schema_migrations/20221128165833 new file mode 100644 index 0000000000000000000000000000000000000000..e2aeaa26c32fa16fe1a40f2237b641bd5e75055e --- /dev/null +++ b/db/schema_migrations/20221128165833 @@ -0,0 +1 @@ +4f4846fe8e5f84ee566dfc8f9b8249e1ff1d77f8f6c2f0006d89a73a2e734b9d \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 513f946eb8335b46df48669e89ff51ddb392e586..5fd2e51ee08d1aa67408cb8178f6bb75ffef2f32 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -22666,9 +22666,11 @@ CREATE TABLE user_details ( organization text DEFAULT ''::text NOT NULL, password_last_changed_at timestamp with time zone DEFAULT now() NOT NULL, onboarding_step_url text, + discord text DEFAULT ''::text NOT NULL, CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)), CONSTRAINT check_444573ee52 CHECK ((char_length(skype) <= 500)), CONSTRAINT check_466a25be35 CHECK ((char_length(twitter) <= 500)), + CONSTRAINT check_4ef1de1a15 CHECK ((char_length(discord) <= 500)), CONSTRAINT check_4f51129940 CHECK ((char_length(onboarding_step_url) <= 2000)), CONSTRAINT check_7b246dad73 CHECK ((char_length(organization) <= 500)), CONSTRAINT check_7d6489f8f3 CHECK ((char_length(linkedin) <= 500)), diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 0179ab03dbefc95e99fb44188cc0075087b6c856..f98399378340c214dd553f3e6686d38863893698 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -132,8 +132,9 @@ To add links to other accounts: 1. On the top bar, in the top-right corner, select your avatar. 1. Select **Edit profile**. 1. In the **Main settings** section, add your information from: - - Skype + - Discord ([User ID](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-)) - LinkedIn + - Skype - Twitter 1. Select **Update profile settings**. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1953c2320a4ef9a174992694758816fc1c4ff003..db2604712ed6ace9893a60054c9b5b0239dff067 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -32678,6 +32678,9 @@ msgstr "" msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account." msgstr "" +msgid "Profiles|Your Discord user ID. Should be between %{min} and %{max} digits long. %{external_accounts_link_start}Learn more.%{external_accounts_link_end}" +msgstr "" + msgid "Profiles|Your LinkedIn profile name from linkedin.com/in/profilename" msgstr "" @@ -50791,6 +50794,9 @@ msgstr "" msgid "must belong to same project of the work item." msgstr "" +msgid "must contain only a discord user ID." +msgstr "" + msgid "must have a repository" msgstr "" diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb index aa92ff6be3329745031098484680f78093f3570d..daf0f36c28b1af1807fbb1f2c47f53cdbe1b8f2c 100644 --- a/spec/controllers/profiles_controller_spec.rb +++ b/spec/controllers/profiles_controller_spec.rb @@ -126,6 +126,16 @@ expect(user.reload.pronunciation).to eq(pronunciation) expect(response).to have_gitlab_http_status(:found) end + + it 'allows updating user specified Discord User ID', :aggregate_failures do + discord_user_id = '1234567890123456789' + sign_in(user) + + put :update, params: { user: { discord: discord_user_id } } + + expect(user.reload.discord).to eq(discord_user_id) + expect(response).to have_gitlab_http_status(:found) + end end describe 'GET audit_log' do diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index f628651b4da11cac4e9061da0c7c3d049703018c..4e921fe171993d9fbb92725fd19198cd26c74132 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -540,6 +540,23 @@ def stub_controller_method(method_name, value) end end + describe '#profile_social_links' do + context 'when discord is set' do + let_it_be(:user) { build(:user) } + let(:discord) { discord_url(user) } + + it 'returns an empty string if discord is not set' do + expect(discord).to eq('') + end + + it 'returns discord url when discord id is set' do + user.discord = '1234567890123456789' + + expect(discord).to eq('https://discord.com/users/1234567890123456789') + end + end + end + describe '#gitlab_ui_form_for' do let_it_be(:user) { build(:user) } diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb index 1893b6530a5ec6a059617973c66a811a1ad738a2..7d433896cf81fcd697de51ed92ea7270d648fef3 100644 --- a/spec/models/user_detail_spec.rb +++ b/spec/models/user_detail_spec.rb @@ -38,6 +38,27 @@ it { is_expected.to validate_length_of(:skype).is_at_most(500) } end + describe '#discord' do + it { is_expected.to validate_length_of(:discord).is_at_most(500) } + + context 'when discord is set' do + let_it_be(:user_detail) { create(:user_detail) } + + it 'accepts a valid discord user id' do + user_detail.discord = '1234567890123456789' + + expect(user_detail).to be_valid + end + + it 'throws an error when other url format is wrong' do + user_detail.discord = '123456789' + + expect(user_detail).not_to be_valid + expect(user_detail.errors.full_messages).to match_array([_('Discord must contain only a discord user ID.')]) + end + end + end + describe '#location' do it { is_expected.to validate_length_of(:location).is_at_most(500) } end @@ -72,11 +93,12 @@ let(:user_detail) do create(:user_detail, bio: 'bio', + discord: '1234567890123456789', linkedin: 'linkedin', - twitter: 'twitter', - skype: 'skype', location: 'location', organization: 'organization', + skype: 'skype', + twitter: 'twitter', website_url: 'https://example.com') end @@ -90,11 +112,12 @@ end it_behaves_like 'prevents `nil` value', :bio + it_behaves_like 'prevents `nil` value', :discord it_behaves_like 'prevents `nil` value', :linkedin - it_behaves_like 'prevents `nil` value', :twitter - it_behaves_like 'prevents `nil` value', :skype it_behaves_like 'prevents `nil` value', :location it_behaves_like 'prevents `nil` value', :organization + it_behaves_like 'prevents `nil` value', :skype + it_behaves_like 'prevents `nil` value', :twitter it_behaves_like 'prevents `nil` value', :website_url end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 5570eb36c8199f617bf7a04b087139e23acb8936..7b5b8acdb66a7ee2a324358358b943ac35b61ad7 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -102,6 +102,9 @@ it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil } + it { is_expected.to delegate_method(:discord).to(:user_detail).allow_nil } + it { is_expected.to delegate_method(:discord=).to(:user_detail).with_arguments(:args).allow_nil } + it { is_expected.to delegate_method(:linkedin).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:linkedin=).to(:user_detail).with_arguments(:args).allow_nil }