diff --git a/app/assets/javascripts/discourse/app/controllers/avatar-selector.js b/app/assets/javascripts/discourse/app/controllers/avatar-selector.js index 83121e0468e..8373b7a106d 100644 --- a/app/assets/javascripts/discourse/app/controllers/avatar-selector.js +++ b/app/assets/javascripts/discourse/app/controllers/avatar-selector.js @@ -17,15 +17,43 @@ export default Controller.extend(ModalFunctionality, { }, @discourseComputed( - "siteSettings.selectable_avatars_enabled", + "siteSettings.selectable_avatars_mode", "siteSettings.selectable_avatars" ) - selectableAvatars(enabled, list) { - if (enabled) { + selectableAvatars(mode, list) { + if (mode !== "disabled") { return list ? list.split("|") : []; } }, + @discourseComputed("siteSettings.selectable_avatars_mode") + showSelectableAvatars(mode) { + return mode !== "disabled"; + }, + + @discourseComputed("siteSettings.selectable_avatars_mode") + showAvatarUploader(mode) { + switch (mode) { + case "no_one": + return false; + case "tl1": + case "tl2": + case "tl3": + case "tl4": + const allowedTl = parseInt(mode.replace("tl", ""), 10); + return ( + this.user.admin || + this.user.moderator || + this.user.trust_level >= allowedTl + ); + case "staff": + return this.user.admin || this.user.moderator; + case "everyone": + default: + return true; + } + }, + @discourseComputed( "user.use_logo_small_as_avatar", "user.avatar_template", diff --git a/app/assets/javascripts/discourse/app/templates/modal/avatar-selector.hbs b/app/assets/javascripts/discourse/app/templates/modal/avatar-selector.hbs index 18d845b9a3c..e7dd9107d19 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/avatar-selector.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/avatar-selector.hbs @@ -1,5 +1,5 @@ {{#d-modal-body title="user.change_avatar.title" class="avatar-selector"}} - {{#if siteSettings.selectable_avatars_enabled}} + {{#if showSelectableAvatars}}
{{#each selectableAvatars as |avatar|}} @@ -7,7 +7,11 @@ {{/each}}
- {{else}} + {{#if showAvatarUploader}} +

{{html-safe (i18n "user.change_avatar.use_custom")}}

+ {{/if}} + {{/if}} + {{#if showAvatarUploader}} {{#if user.use_logo_small_as_avatar}}
{{radio-button id="logo-small" name="logo" value="logo" selection=selected}} @@ -55,9 +59,9 @@ {{/if}} {{/d-modal-body}} -{{#unless siteSettings.selectable_avatars_enabled}} +{{#if showAvatarUploader}} -{{/unless}} +{{/if}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js index ea04050eb11..023ab689217 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js @@ -299,7 +299,7 @@ acceptance( "Avatar selector when selectable avatars is enabled", function (needs) { needs.user(); - needs.settings({ selectable_avatars_enabled: true }); + needs.settings({ selectable_avatars_mode: "no_one" }); needs.pretender((server, helper) => { server.get("/site/selectable-avatars.json", () => helper.response([ @@ -315,6 +315,134 @@ acceptance( assert.ok( exists(".selectable-avatars", "opens the avatar selection modal") ); + assert.notOk( + exists( + "#uploaded-avatar", + "avatar selection modal does not include option to upload" + ) + ); + }); + } +); +acceptance( + "Avatar selector when selectable avatars allows staff to upload", + function (needs) { + needs.user(); + needs.settings({ selectable_avatars_mode: "staff" }); + needs.pretender((server, helper) => { + server.get("/site/selectable-avatars.json", () => + helper.response([ + "https://www.discourse.org", + "https://meta.discourse.org", + ]) + ); + }); + + test("allows staff to upload", async function (assert) { + await updateCurrentUser({ + trust_level: 3, + moderator: true, + admin: false, + }); + await visit("/u/eviltrout/preferences"); + await click(".pref-avatar .btn"); + assert.ok( + exists(".selectable-avatars", "opens the avatar selection modal") + ); + assert.ok( + exists( + "#uploaded-avatar", + "avatar selection modal includes option to upload" + ) + ); + }); + test("disallow nonstaff", async function (assert) { + await visit("/u/eviltrout/preferences"); + await updateCurrentUser({ + trust_level: 3, + moderator: false, + admin: false, + }); + await click(".pref-avatar .btn"); + assert.ok( + exists(".selectable-avatars", "opens the avatar selection modal") + ); + assert.notOk( + exists( + "#uploaded-avatar", + "avatar selection modal does not include option to upload" + ) + ); + }); + } +); +acceptance( + "Avatar selector when selectable avatars allows trust level 3+ to upload", + function (needs) { + needs.user(); + needs.settings({ selectable_avatars_mode: "tl3" }); + needs.pretender((server, helper) => { + server.get("/site/selectable-avatars.json", () => + helper.response([ + "https://www.discourse.org", + "https://meta.discourse.org", + ]) + ); + }); + + test("with a tl3 user", async function (assert) { + await visit("/u/eviltrout/preferences"); + await updateCurrentUser({ + trust_level: 3, + moderator: false, + admin: false, + }); + await click(".pref-avatar .btn"); + assert.ok( + exists(".selectable-avatars", "opens the avatar selection modal") + ); + assert.ok( + exists( + "#uploaded-avatar", + "avatar selection modal does includes option to upload" + ) + ); + }); + test("with a tl2 user", async function (assert) { + await visit("/u/eviltrout/preferences"); + await updateCurrentUser({ + trust_level: 2, + moderator: false, + admin: false, + }); + await click(".pref-avatar .btn"); + assert.ok( + exists(".selectable-avatars", "opens the avatar selection modal") + ); + assert.notOk( + exists( + "#uploaded-avatar", + "avatar selection modal does not include option to upload" + ) + ); + }); + test("always allow staff to upload", async function (assert) { + await visit("/u/eviltrout/preferences"); + await updateCurrentUser({ + trust_level: 2, + moderator: true, + admin: false, + }); + await click(".pref-avatar .btn"); + assert.ok( + exists(".selectable-avatars", "opens the avatar selection modal") + ); + assert.ok( + exists( + "#uploaded-avatar", + "avatar selection modal includes option to upload" + ) + ); }); } ); diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 493cbd112b4..b329dd53cf2 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1244,7 +1244,7 @@ class UsersController < ApplicationController return render json: failed_json, status: 422 end - unless SiteSetting.selectable_avatars_enabled + if SiteSetting.selectable_avatars_mode == "disabled" return render json: failed_json, status: 422 end diff --git a/app/models/user.rb b/app/models/user.rb index f4f80e70e7b..0ab48be074d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1261,7 +1261,7 @@ class User < ActiveRecord::Base end def set_random_avatar - if SiteSetting.selectable_avatars_enabled? + if SiteSetting.selectable_avatars_mode != "disabled" if upload = SiteSetting.selectable_avatars.sample update_column(:uploaded_avatar_id, upload.id) UserAvatar.create!(user_id: id, custom_upload_id: upload.id) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 5ef7ed8fd6b..e7c0e941fb7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1381,6 +1381,7 @@ en: upload_title: "Upload your picture" image_is_not_a_square: "Warning: we've cropped your image; width and height were not equal." logo_small: "Site's small logo. Used by default." + use_custom: "Or upload a custom avatar:" change_profile_background: title: "Profile Header" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index d6b0f60d385..70f46f48cf6 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1828,7 +1828,7 @@ en: use_site_small_logo_as_system_avatar: "Use the site's small logo instead of the system user's avatar. Requires the logo to be present." restrict_letter_avatar_colors: "A list of 6-digit hexadecimal color values to be used for letter avatar background." enable_listing_suspended_users_on_search: "Enable regular users to find suspended users." - selectable_avatars_enabled: "Force users to choose an avatar from the list." + selectable_avatars_mode: "Allow users to select an avatar from the selectable_avatars list and limit custom avatar uploads to the selected trust level." selectable_avatars: "List of avatars users can choose from." allow_all_attachments_for_group_messages: "Allow all email attachments for group messages." diff --git a/config/site_settings.yml b/config/site_settings.yml index 36af41ffdcc..922a5d28fbe 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1397,10 +1397,20 @@ files: type: list list_type: compact validator: "ColorListValidator" - selectable_avatars_enabled: - default: false + selectable_avatars_mode: + default: disabled client: true - validator: "SelectableAvatarsEnabledValidator" + type: enum + choices: + - disabled + - everyone + - tl1 + - tl2 + - tl3 + - tl4 + - staff + - no_one + validator: "SelectableAvatarsModeValidator" selectable_avatars: default: "" client: true diff --git a/db/post_migrate/20220202223955_migrate_selectable_avatars_enabled.rb b/db/post_migrate/20220202223955_migrate_selectable_avatars_enabled.rb new file mode 100644 index 00000000000..5b67f9041ce --- /dev/null +++ b/db/post_migrate/20220202223955_migrate_selectable_avatars_enabled.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class MigrateSelectableAvatarsEnabled < ActiveRecord::Migration[6.1] + def up + execute <<~SQL + UPDATE site_settings AS s + SET value = + CASE WHEN t.value = 't' THEN 'no_one' + ELSE 'disabled' + END, + data_type = #{SiteSettings::TypeSupervisor.types[:enum]}, + name = 'selectable_avatars_mode' + FROM site_settings t + WHERE s.id = t.id AND s.name = 'selectable_avatars_enabled' + SQL + end + + def down + execute <<~SQL + UPDATE site_settings AS s + SET value = + CASE WHEN t.value IN ('everyone', 'no_one', 'staff', 'tl1','tl2', 'tl3', 'tl4') THEN 't' + ELSE 'f' + END, + data_type = #{SiteSettings::TypeSupervisor.types[:bool]}, + name = 'selectable_avatars_enabled' + FROM site_settings t + WHERE s.id = t.id AND s.name = 'selectable_avatars_mode' + SQL + end +end diff --git a/lib/validators/selectable_avatars_enabled_validator.rb b/lib/validators/selectable_avatars_mode_validator.rb similarity index 66% rename from lib/validators/selectable_avatars_enabled_validator.rb rename to lib/validators/selectable_avatars_mode_validator.rb index cc54c9ce5d1..dcf46e7d21a 100644 --- a/lib/validators/selectable_avatars_enabled_validator.rb +++ b/lib/validators/selectable_avatars_mode_validator.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -class SelectableAvatarsEnabledValidator +class SelectableAvatarsModeValidator def initialize(opts = {}) @opts = opts end def valid_value?(value) - value == "f" || SiteSetting.selectable_avatars.size > 1 + value == "disabled" || SiteSetting.selectable_avatars.size > 1 end def error_message diff --git a/spec/components/validators/selectable_avatars_mode_validator_spec.rb b/spec/components/validators/selectable_avatars_mode_validator_spec.rb new file mode 100644 index 00000000000..cfdb0ed2955 --- /dev/null +++ b/spec/components/validators/selectable_avatars_mode_validator_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe SelectableAvatarsModeValidator do + describe '#valid_value?' do + subject(:validator) { described_class.new } + + it "returns true when disabling" do + SiteSetting.selectable_avatars = "" + expect(validator.valid_value?("disabled")).to eq(true) + + SiteSetting.selectable_avatars = [Fabricate(:image_upload), Fabricate(:image_upload)] + expect(validator.valid_value?("disabled")).to eq(true) + end + + it "returns true when there are at least two selectable avatars" do + SiteSetting.selectable_avatars = [Fabricate(:image_upload), Fabricate(:image_upload)] + expect(validator.valid_value?("no_one")).to eq(true) + end + + it "returns false when selectable avatars is blank or has one avatar" do + SiteSetting.selectable_avatars = "" + expect(validator.valid_value?("no_one")).to eq(false) + + SiteSetting.selectable_avatars = [Fabricate(:image_upload)] + expect(validator.valid_value?("no_one")).to eq(false) + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9b2c2e1f3a5..3e67f029abc 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2141,7 +2141,7 @@ describe User do avatar1 = Fabricate(:upload) avatar2 = Fabricate(:upload) SiteSetting.selectable_avatars = [avatar1, avatar2] - SiteSetting.selectable_avatars_enabled = true + SiteSetting.selectable_avatars_mode = "no_one" user = Fabricate(:user) expect(user.uploaded_avatar_id).not_to be(nil) diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 8027d38520c..2ca1107f5d6 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -2622,7 +2622,7 @@ describe UsersController do before do SiteSetting.selectable_avatars = [avatar1, avatar2] - SiteSetting.selectable_avatars_enabled = true + SiteSetting.selectable_avatars_mode = "no_one" end it 'raises an error when selectable avatars is empty' do