FEATURE: upload an avatar option for uploading avatars with selectable avatars (#15878)

* FEATURE: upload an avatar option for uploading avatars with selectable avatars

Allow staff or users at or above a trust level to upload avatars even when the site
has selectable avatars enabled.

Everyone can still pick from the list of avatars. The option to upload is shown
below the selectable avatar list.

refactored boolean site setting into an enum with the following values:

disabled: No selectable avatars enabled (default)
everyone: Show selectable avatars, and allow everyone to upload custom avatars
tl1: Show selectable avatars, but require tl1+ and staff to upload custom avatars
tl2: Show selectable avatars, but require tl2+ and staff to upload custom avatars
tl3: Show selectable avatars, but require tl3+ and staff to upload custom avatars
tl4: Show selectable avatars, but require tl4 and staff to upload custom avatars
staff: Show selectable avatars, but only allow staff to upload custom avatars
no_one: Show selectable avatars. No users can upload custom avatars

Co-authored-by: Régis Hanol <regis@hanol.fr>
This commit is contained in:
Jeff Wong 2022-02-24 10:57:39 -10:00 committed by GitHub
parent 00bb5f3a9d
commit d1bdb6c65d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 250 additions and 18 deletions

View File

@ -17,15 +17,43 @@ export default Controller.extend(ModalFunctionality, {
}, },
@discourseComputed( @discourseComputed(
"siteSettings.selectable_avatars_enabled", "siteSettings.selectable_avatars_mode",
"siteSettings.selectable_avatars" "siteSettings.selectable_avatars"
) )
selectableAvatars(enabled, list) { selectableAvatars(mode, list) {
if (enabled) { if (mode !== "disabled") {
return list ? list.split("|") : []; 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( @discourseComputed(
"user.use_logo_small_as_avatar", "user.use_logo_small_as_avatar",
"user.avatar_template", "user.avatar_template",

View File

@ -1,5 +1,5 @@
{{#d-modal-body title="user.change_avatar.title" class="avatar-selector"}} {{#d-modal-body title="user.change_avatar.title" class="avatar-selector"}}
{{#if siteSettings.selectable_avatars_enabled}} {{#if showSelectableAvatars}}
<div class="selectable-avatars"> <div class="selectable-avatars">
{{#each selectableAvatars as |avatar|}} {{#each selectableAvatars as |avatar|}}
<a href class="selectable-avatar" {{action "selectAvatar" avatar}}> <a href class="selectable-avatar" {{action "selectAvatar" avatar}}>
@ -7,7 +7,11 @@
</a> </a>
{{/each}} {{/each}}
</div> </div>
{{else}} {{#if showAvatarUploader}}
<h4>{{html-safe (i18n "user.change_avatar.use_custom")}}</h4>
{{/if}}
{{/if}}
{{#if showAvatarUploader}}
{{#if user.use_logo_small_as_avatar}} {{#if user.use_logo_small_as_avatar}}
<div class="avatar-choice"> <div class="avatar-choice">
{{radio-button id="logo-small" name="logo" value="logo" selection=selected}} {{radio-button id="logo-small" name="logo" value="logo" selection=selected}}
@ -55,9 +59,9 @@
{{/if}} {{/if}}
{{/d-modal-body}} {{/d-modal-body}}
{{#unless siteSettings.selectable_avatars_enabled}} {{#if showAvatarUploader}}
<div class="modal-footer"> <div class="modal-footer">
{{d-button action=(action "saveAvatarSelection") class="btn-primary" disabled=submitDisabled label="save"}} {{d-button action=(action "saveAvatarSelection") class="btn-primary" disabled=submitDisabled label="save"}}
{{d-modal-cancel close=(route-action "closeModal")}} {{d-modal-cancel close=(route-action "closeModal")}}
</div> </div>
{{/unless}} {{/if}}

View File

@ -299,7 +299,7 @@ acceptance(
"Avatar selector when selectable avatars is enabled", "Avatar selector when selectable avatars is enabled",
function (needs) { function (needs) {
needs.user(); needs.user();
needs.settings({ selectable_avatars_enabled: true }); needs.settings({ selectable_avatars_mode: "no_one" });
needs.pretender((server, helper) => { needs.pretender((server, helper) => {
server.get("/site/selectable-avatars.json", () => server.get("/site/selectable-avatars.json", () =>
helper.response([ helper.response([
@ -315,6 +315,134 @@ acceptance(
assert.ok( assert.ok(
exists(".selectable-avatars", "opens the avatar selection modal") 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"
)
);
}); });
} }
); );

View File

@ -1244,7 +1244,7 @@ class UsersController < ApplicationController
return render json: failed_json, status: 422 return render json: failed_json, status: 422
end end
unless SiteSetting.selectable_avatars_enabled if SiteSetting.selectable_avatars_mode == "disabled"
return render json: failed_json, status: 422 return render json: failed_json, status: 422
end end

View File

@ -1261,7 +1261,7 @@ class User < ActiveRecord::Base
end end
def set_random_avatar def set_random_avatar
if SiteSetting.selectable_avatars_enabled? if SiteSetting.selectable_avatars_mode != "disabled"
if upload = SiteSetting.selectable_avatars.sample if upload = SiteSetting.selectable_avatars.sample
update_column(:uploaded_avatar_id, upload.id) update_column(:uploaded_avatar_id, upload.id)
UserAvatar.create!(user_id: id, custom_upload_id: upload.id) UserAvatar.create!(user_id: id, custom_upload_id: upload.id)

View File

@ -1381,6 +1381,7 @@ en:
upload_title: "Upload your picture" upload_title: "Upload your picture"
image_is_not_a_square: "Warning: we've cropped your image; width and height were not equal." 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." logo_small: "Site's small logo. Used by default."
use_custom: "Or upload a custom avatar:"
change_profile_background: change_profile_background:
title: "Profile Header" title: "Profile Header"

View File

@ -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." 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." 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." 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." selectable_avatars: "List of avatars users can choose from."
allow_all_attachments_for_group_messages: "Allow all email attachments for group messages." allow_all_attachments_for_group_messages: "Allow all email attachments for group messages."

View File

@ -1397,10 +1397,20 @@ files:
type: list type: list
list_type: compact list_type: compact
validator: "ColorListValidator" validator: "ColorListValidator"
selectable_avatars_enabled: selectable_avatars_mode:
default: false default: disabled
client: true client: true
validator: "SelectableAvatarsEnabledValidator" type: enum
choices:
- disabled
- everyone
- tl1
- tl2
- tl3
- tl4
- staff
- no_one
validator: "SelectableAvatarsModeValidator"
selectable_avatars: selectable_avatars:
default: "" default: ""
client: true client: true

View File

@ -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

View File

@ -1,12 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class SelectableAvatarsEnabledValidator class SelectableAvatarsModeValidator
def initialize(opts = {}) def initialize(opts = {})
@opts = opts @opts = opts
end end
def valid_value?(value) def valid_value?(value)
value == "f" || SiteSetting.selectable_avatars.size > 1 value == "disabled" || SiteSetting.selectable_avatars.size > 1
end end
def error_message def error_message

View File

@ -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

View File

@ -2141,7 +2141,7 @@ describe User do
avatar1 = Fabricate(:upload) avatar1 = Fabricate(:upload)
avatar2 = Fabricate(:upload) avatar2 = Fabricate(:upload)
SiteSetting.selectable_avatars = [avatar1, avatar2] SiteSetting.selectable_avatars = [avatar1, avatar2]
SiteSetting.selectable_avatars_enabled = true SiteSetting.selectable_avatars_mode = "no_one"
user = Fabricate(:user) user = Fabricate(:user)
expect(user.uploaded_avatar_id).not_to be(nil) expect(user.uploaded_avatar_id).not_to be(nil)

View File

@ -2622,7 +2622,7 @@ describe UsersController do
before do before do
SiteSetting.selectable_avatars = [avatar1, avatar2] SiteSetting.selectable_avatars = [avatar1, avatar2]
SiteSetting.selectable_avatars_enabled = true SiteSetting.selectable_avatars_mode = "no_one"
end end
it 'raises an error when selectable avatars is empty' do it 'raises an error when selectable avatars is empty' do