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}}
- {{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