FEATURE: selectable avatars

This commit is contained in:
Régis Hanol
2018-07-18 12:57:43 +02:00
parent a24b9981c6
commit 6d6e026e3c
28 changed files with 435 additions and 80 deletions

View File

@ -0,0 +1,15 @@
import showModal from "discourse/lib/show-modal";
export default Ember.Component.extend({
actions: {
showUploadModal({ value, setting }) {
showModal("admin-uploaded-image-list", {
admin: true,
title: `admin.site_settings.${setting.setting}.title`,
model: { value, setting },
}).setProperties({
save: v => this.set("value", v)
});
}
}
});

View File

@ -0,0 +1,27 @@
import { on, observes } from "ember-addons/ember-computed-decorators";
import ModalFunctionality from 'discourse/mixins/modal-functionality';
export default Ember.Controller.extend(ModalFunctionality, {
@on("init")
@observes("model.value")
_setup() {
const value = this.get("model.value");
this.set("images", value && value.length ? value.split("\n") : []);
},
actions: {
uploadDone({ url }) {
this.get("images").addObject(url);
},
remove(url) {
this.get("images").removeObject(url);
},
close() {
this.save(this.get("images").join("\n"));
this.send("closeModal");
}
}
});

View File

@ -9,7 +9,8 @@ const CUSTOM_TYPES = [
"host_list", "host_list",
"category_list", "category_list",
"value_list", "value_list",
"category" "category",
"uploaded_image_list",
]; ];
export default Ember.Mixin.create({ export default Ember.Mixin.create({

View File

@ -0,0 +1,2 @@
{{d-button label="admin.site_settings.uploaded_image_list.label" action="showUploadModal" actionParam=(hash value=value setting=setting)}}
<div class='desc'>{{{unbound setting.description}}}</div>

View File

@ -0,0 +1,15 @@
{{#d-modal-body class="uploaded-image-list"}}
<div class="selectable-avatars">
{{#each images as |image|}}
<div class="selectable-avatar" {{action "remove" image}}>
{{bound-avatar-template image "huge"}}
</div>
{{else}}
<p>{{i18n "admin.site_settings.uploaded_image_list.empty"}}</p>
{{/each}}
</div>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action=(action "close") label="close"}}
{{images-uploader uploading=uploading done="uploadDone" class="pull-right"}}
</div>

View File

@ -0,0 +1,20 @@
import computed from "ember-addons/ember-computed-decorators";
import UploadMixin from "discourse/mixins/upload";
export default Em.Component.extend(UploadMixin, {
type: "avatar",
tagName: "span",
@computed("uploading")
uploadButtonText(uploading) {
return uploading ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
},
validateUploadedFilesOptions() {
return { imagesOnly: true };
},
uploadDone(upload) {
this.sendAction("done", upload);
},
});

View File

@ -1,7 +1,6 @@
import computed from "ember-addons/ember-computed-decorators"; import computed from "ember-addons/ember-computed-decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality"; import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { allowsImages } from "discourse/lib/utilities"; import { allowsImages } from "discourse/lib/utilities";
export default Ember.Controller.extend(ModalFunctionality, { export default Ember.Controller.extend(ModalFunctionality, {

View File

@ -508,19 +508,21 @@ const User = RestModel.extend({
data: { upload_id, type } data: { upload_id, type }
} }
).then(() => ).then(() =>
this.setProperties({ this.setProperties({ avatar_template, uploaded_avatar_id: upload_id })
avatar_template,
uploaded_avatar_id: upload_id
})
); );
}, },
selectAvatar(avatarUrl) {
return ajax(
userPath(`${this.get("username_lower")}/preferences/avatar/select`),
{ type: "PUT", data: { url: avatarUrl } }
).then(result => this.setProperties(result));
},
isAllowedToUploadAFile(type) { isAllowedToUploadAFile(type) {
return ( return this.get("staff") ||
this.get("staff") ||
this.get("trust_level") > 0 || this.get("trust_level") > 0 ||
Discourse.SiteSettings["newuser_max_" + type + "s"] > 0 Discourse.SiteSettings[`newuser_max_${type}s`] > 0;
);
}, },
createInvite(email, group_names, custom_message) { createInvite(email, group_names, custom_message) {

View File

@ -1,23 +1,15 @@
import RestrictedUserRoute from "discourse/routes/restricted-user"; import RestrictedUserRoute from "discourse/routes/restricted-user";
import showModal from "discourse/lib/show-modal"; import showModal from "discourse/lib/show-modal";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { ajax } from "discourse/lib/ajax";
export default RestrictedUserRoute.extend({ export default RestrictedUserRoute.extend({
model() { model() {
return this.modelFor("user"); return this.modelFor("user");
}, },
setupController(controller, user) {
controller.setProperties({
model: user
});
},
actions: { actions: {
showAvatarSelector() { showAvatarSelector() {
showModal("avatar-selector");
// all the properties needed for displaying the avatar selector modal
const props = this.modelFor("user").getProperties( const props = this.modelFor("user").getProperties(
"id", "id",
"email", "email",
@ -42,15 +34,32 @@ export default RestrictedUserRoute.extend({
props.selected = "uploaded"; props.selected = "uploaded";
} }
this.controllerFor("avatar-selector").setProperties(props); const controller = showModal("avatar-selector");
controller.setProperties(props);
if (this.siteSettings.selectable_avatars_enabled) {
ajax("/site/selectable-avatars.json")
.then(avatars => controller.set("selectableAvatars", avatars));
}
},
selectAvatar(url) {
const user = this.modelFor("user");
const controller = this.controllerFor("avatar-selector");
user
.selectAvatar(url)
.then(() => bootbox.alert(I18n.t("user.change_avatar.cache_notice")))
.catch(popupAjaxError)
.finally(() => controller.send("closeModal"));
}, },
saveAvatarSelection() { saveAvatarSelection() {
const user = this.modelFor("user"), const user = this.modelFor("user");
controller = this.controllerFor("avatar-selector"), const controller = this.controllerFor("avatar-selector");
selectedUploadId = controller.get("selectedUploadId"), const selectedUploadId = controller.get("selectedUploadId");
selectedAvatarTemplate = controller.get("selectedAvatarTemplate"), const selectedAvatarTemplate = controller.get("selectedAvatarTemplate");
type = controller.get("selected"); const type = controller.get("selected");
user user
.pickAvatar(selectedUploadId, type, selectedAvatarTemplate) .pickAvatar(selectedUploadId, type, selectedAvatarTemplate)
@ -64,10 +73,8 @@ export default RestrictedUserRoute.extend({
); );
bootbox.alert(I18n.t("user.change_avatar.cache_notice")); bootbox.alert(I18n.t("user.change_avatar.cache_notice"));
}) })
.catch(popupAjaxError); .catch(popupAjaxError)
.finally(() => controller.send("closeModal"));
// saves the data back
controller.send("closeModal");
} }
} }
}); });

View File

@ -0,0 +1,7 @@
<label class="btn" disabled={{uploading}} title="{{i18n "admin.site_settings.uploaded_image_list.upload.title"}}">
{{d-icon "picture-o"}}&nbsp;{{uploadButtonText}}
<input disabled={{uploading}} type="file" accept="image/*" multiple style="visibility: hidden; position: absolute;" />
</label>
{{#if uploading}}
<span>{{i18n 'upload_selector.uploading'}} {{uploadProgress}}%</span>
{{/if}}

View File

@ -1,37 +1,49 @@
{{#d-modal-body title="user.change_avatar.title" class="avatar-selector"}} {{#d-modal-body title="user.change_avatar.title" class="avatar-selector"}}
<div class="avatar-choice"> {{#if siteSettings.selectable_avatars_enabled}}
{{radio-button id="system-avatar" name="avatar" value="system" selection=selected}} <div class="selectable-avatars">
<label class="radio" for="system-avatar">{{bound-avatar-template system_avatar_template "large"}} {{{i18n 'user.change_avatar.letter_based'}}}</label> {{#each selectableAvatars as |avatar|}}
</div> <div class="selectable-avatar" {{action "selectAvatar" avatar}}>
<div class="avatar-choice"> {{bound-avatar-template avatar "huge"}}
{{radio-button id="gravatar" name="avatar" value="gravatar" selection=selected}} </div>
<label class="radio" for="gravatar">{{bound-avatar-template gravatar_avatar_template "large"}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label> {{/each}}
{{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}}
{{#if gravatarFailed}}
<p class="error">{{I18n 'user.change_avatar.gravatar_failed'}}</p>
{{/if}}
</div>
{{#if allowAvatarUpload}}
<div class="avatar-choice">
{{radio-button id="uploaded-avatar" name="avatar" value="uploaded" selection=selected}}
<label class="radio" for="uploaded-avatar">
{{#if custom_avatar_template}}
{{bound-avatar-template custom_avatar_template "large"}}
{{i18n 'user.change_avatar.uploaded_avatar'}}
{{else}}
{{i18n 'user.change_avatar.uploaded_avatar_empty'}}
{{/if}}
</label>
{{avatar-uploader user_id=id
uploadedAvatarTemplate=custom_avatar_template
uploadedAvatarId=custom_avatar_upload_id
uploading=uploading
done="uploadComplete"}}
</div> </div>
{{else}}
<div class="avatar-choice">
{{radio-button id="system-avatar" name="avatar" value="system" selection=selected}}
<label class="radio" for="system-avatar">{{bound-avatar-template system_avatar_template "large"}} {{{i18n 'user.change_avatar.letter_based'}}}</label>
</div>
<div class="avatar-choice">
{{radio-button id="gravatar" name="avatar" value="gravatar" selection=selected}}
<label class="radio" for="gravatar">{{bound-avatar-template gravatar_avatar_template "large"}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label>
{{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}}
{{#if gravatarFailed}}
<p class="error">{{I18n 'user.change_avatar.gravatar_failed'}}</p>
{{/if}}
</div>
{{#if allowAvatarUpload}}
<div class="avatar-choice">
{{radio-button id="uploaded-avatar" name="avatar" value="uploaded" selection=selected}}
<label class="radio" for="uploaded-avatar">
{{#if custom_avatar_template}}
{{bound-avatar-template custom_avatar_template "large"}}
{{i18n 'user.change_avatar.uploaded_avatar'}}
{{else}}
{{i18n 'user.change_avatar.uploaded_avatar_empty'}}
{{/if}}
</label>
{{avatar-uploader user_id=id
uploadedAvatarTemplate=custom_avatar_template
uploadedAvatarId=custom_avatar_upload_id
uploading=uploading
done="uploadComplete"}}
</div>
{{/if}}
{{/if}} {{/if}}
{{/d-modal-body}} {{/d-modal-body}}
<div class="modal-footer"> {{#unless siteSettings.selectable_avatars_enabled}}
{{d-button action="saveAvatarSelection" class="btn-primary" disabled=uploading label="save"}} <div class="modal-footer">
{{d-modal-cancel close=(action "closeModal")}} {{d-button action="saveAvatarSelection" class="btn-primary" disabled=uploading label="save"}}
</div> {{d-modal-cancel close=(action "closeModal")}}
</div>
{{/unless}}

View File

@ -106,3 +106,4 @@
color: $danger; color: $danger;
} }
} }

View File

@ -661,3 +661,20 @@
#user-card .staged { #user-card .staged {
font-style: italic; font-style: italic;
} }
.selectable-avatars {
max-height: 350px;
margin-bottom: 1em;
text-align: justify;
.selectable-avatar {
margin: 5px;
display: inline-block;
.avatar {
width: 60px;
height: 60px;
&:hover {
box-shadow: 0 0 10px $primary;
}
}
}
}

View File

@ -25,6 +25,16 @@ class SiteController < ApplicationController
render json: custom_emoji render json: custom_emoji
end end
def selectable_avatars
avatars = if SiteSetting.selectable_avatars_enabled?
(SiteSetting.selectable_avatars.presence || "").split("\n")
else
[]
end
render json: avatars, root: false
end
def basic_info def basic_info
results = { results = {
logo_url: UrlHelper.absolute(SiteSetting.logo_url), logo_url: UrlHelper.absolute(SiteSetting.logo_url),

View File

@ -11,8 +11,9 @@ class UsersController < ApplicationController
requires_login only: [ requires_login only: [
:username, :update, :user_preferences_redirect, :upload_user_image, :username, :update, :user_preferences_redirect, :upload_user_image,
:pick_avatar, :destroy_user_image, :destroy, :check_emails, :topic_tracking_state, :pick_avatar, :destroy_user_image, :destroy, :check_emails,
:preferences, :create_second_factor, :update_second_factor, :create_second_factor_backup :topic_tracking_state, :preferences, :create_second_factor,
:update_second_factor, :create_second_factor_backup, :select_avatar
] ]
skip_before_action :check_xhr, only: [ skip_before_action :check_xhr, only: [
@ -885,6 +886,46 @@ class UsersController < ApplicationController
render json: success_json render json: success_json
end end
def select_avatar
user = fetch_user_from_params
guardian.ensure_can_edit!(user)
url = params[:url]
if url.blank?
return render json: failed_json, status: 422
end
unless SiteSetting.selectable_avatars_enabled
return render json: failed_json, status: 422
end
if SiteSetting.selectable_avatars.blank?
return render json: failed_json, status: 422
end
unless SiteSetting.selectable_avatars[url]
return render json: failed_json, status: 422
end
unless upload = Upload.find_by(url: url)
return render json: failed_json, status: 422
end
user.uploaded_avatar_id = upload.id
user.save!
avatar = user.user_avatar || user.create_user_avatar
avatar.custom_upload_id = upload.id
avatar.save!
render json: {
avatar_template: user.avatar_template,
custom_avatar_template: user.avatar_template,
uploaded_avatar_id: upload.id,
}
end
def destroy_user_image def destroy_user_image
user = fetch_user_from_params user = fetch_user_from_params
guardian.ensure_can_edit!(user) guardian.ensure_can_edit!(user)

View File

@ -24,7 +24,8 @@ module Jobs
SiteSetting.logo_small_url, SiteSetting.logo_small_url,
SiteSetting.favicon_url, SiteSetting.favicon_url,
SiteSetting.apple_touch_icon_url, SiteSetting.apple_touch_icon_url,
].map do |url| *SiteSetting.selectable_avatars.split("\n"),
].flatten.map do |url|
if url.present? if url.present?
url = url.dup url = url.dup

View File

@ -104,6 +104,7 @@ class User < ActiveRecord::Base
after_create :create_user_stat after_create :create_user_stat
after_create :create_user_option after_create :create_user_option
after_create :create_user_profile after_create :create_user_profile
after_create :set_random_avatar
after_create :ensure_in_trust_level_group after_create :ensure_in_trust_level_group
after_create :set_default_categories_preferences after_create :set_default_categories_preferences
@ -612,8 +613,7 @@ class User < ActiveRecord::Base
end end
def self.gravatar_template(email) def self.gravatar_template(email)
email_hash = self.email_hash(email) "//www.gravatar.com/avatar/#{self.email_hash(email)}.png?s={size}&r=pg&d=identicon"
"//www.gravatar.com/avatar/#{email_hash}.png?s={size}&r=pg&d=identicon"
end end
# Don't pass this up to the client - it's meant for server side use # Don't pass this up to the client - it's meant for server side use
@ -628,19 +628,19 @@ class User < ActiveRecord::Base
UrlHelper.schemaless UrlHelper.absolute avatar_template UrlHelper.schemaless UrlHelper.absolute avatar_template
end end
def self.username_hash(username)
username.each_char.reduce(0) do |result, char|
[((result << 5) - result) + char.ord].pack('L').unpack('l').first
end.abs
end
def self.default_template(username) def self.default_template(username)
if SiteSetting.default_avatars.present? if SiteSetting.default_avatars.present?
split_avatars = SiteSetting.default_avatars.split("\n") urls = SiteSetting.default_avatars.split("\n")
if split_avatars.present? return urls[username_hash(username) % urls.size] if urls.present?
hash = username.each_char.reduce(0) do |result, char|
[((result << 5) - result) + char.ord].pack('L').unpack('l').first
end
split_avatars[hash.abs % split_avatars.size]
end
else
system_avatar_template(username)
end end
system_avatar_template(username)
end end
def self.avatar_template(username, uploaded_avatar_id) def self.avatar_template(username, uploaded_avatar_id)
@ -1018,6 +1018,18 @@ class User < ActiveRecord::Base
UserProfile.create(user_id: id) UserProfile.create(user_id: id)
end end
def set_random_avatar
if SiteSetting.selectable_avatars_enabled? && SiteSetting.selectable_avatars.present?
urls = SiteSetting.selectable_avatars.split("\n")
if urls.present?
if upload = Upload.find_by(url: urls.sample)
update_column(:uploaded_avatar_id, upload.id)
UserAvatar.create(user_id: id, custom_upload_id: upload.id)
end
end
end
end
def anonymous? def anonymous?
SiteSetting.allow_anonymous_posting && SiteSetting.allow_anonymous_posting &&
trust_level >= 1 && trust_level >= 1 &&

View File

@ -3797,6 +3797,14 @@ en:
clear_filter: "Clear" clear_filter: "Clear"
add_url: "add URL" add_url: "add URL"
add_host: "add host" add_host: "add host"
uploaded_image_list:
label: "Edit list"
empty: "There are no pictures yet. Please upload one."
upload:
label: "Upload"
title: "Upload image(s)"
selectable_avatars:
title: "List of avatars users can choose from"
categories: categories:
all_results: 'All' all_results: 'All'
required: 'Required' required: 'Required'

View File

@ -1339,6 +1339,9 @@ en:
external_system_avatars_enabled: "Use external system avatars service." external_system_avatars_enabled: "Use external system avatars service."
external_system_avatars_url: "URL of the external system avatars service. Allowed substitutions are {username} {first_letter} {color} {size}" external_system_avatars_url: "URL of the external system avatars service. Allowed substitutions are {username} {first_letter} {color} {size}"
selectable_avatars_enabled: "Force users to choose an avatar from the list."
selectable_avatars: "List of avatars users can choose from."
default_opengraph_image_url: "URL of the default opengraph image." default_opengraph_image_url: "URL of the default opengraph image."
twitter_summary_large_image_url: "URL of the default Twitter summary card image (should be at least 280px in width, and at least 150px in height)." twitter_summary_large_image_url: "URL of the default Twitter summary card image (should be at least 280px in width, and at least 150px in height)."

View File

@ -54,6 +54,7 @@ Discourse::Application.routes.draw do
get "site/basic-info" => 'site#basic_info' get "site/basic-info" => 'site#basic_info'
get "site/statistics" => 'site#statistics' get "site/statistics" => 'site#statistics'
get "site/selectable-avatars" => "site#selectable_avatars"
get "srv/status" => "forums#status" get "srv/status" => "forums#status"
@ -405,6 +406,7 @@ Discourse::Application.routes.draw do
get "#{root_path}/:username/preferences/second-factor-backup" => "users#preferences", constraints: { username: RouteFormat.username } get "#{root_path}/:username/preferences/second-factor-backup" => "users#preferences", constraints: { username: RouteFormat.username }
delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: RouteFormat.username } delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormat.username } put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/preferences/avatar/select" => "users#select_avatar", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormat.username } get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username } get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username } get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username }

View File

@ -959,6 +959,12 @@ files:
client: true client: true
regex: '^((https?:)?\/)?\/.+[^\/]' regex: '^((https?:)?\/)?\/.+[^\/]'
shadowed_by_global: true shadowed_by_global: true
selectable_avatars_enabled:
default: false
client: true
selectable_avatars:
default: ''
type: uploaded_image_list
allow_all_attachments_for_group_messages: false allow_all_attachments_for_group_messages: false
png_to_jpg_quality: png_to_jpg_quality:
default: 95 default: 95

View File

@ -29,7 +29,8 @@ class SiteSettings::TypeSupervisor
regex: 13, regex: 13,
email: 14, email: 14,
username: 15, username: 15,
category: 16 category: 16,
uploaded_image_list: 17,
) )
end end

View File

@ -73,6 +73,12 @@ describe SiteSettings::TypeSupervisor do
it "'username' should be at 15th position" do it "'username' should be at 15th position" do
expect(SiteSettings::TypeSupervisor.types[:username]).to eq(15) expect(SiteSettings::TypeSupervisor.types[:username]).to eq(15)
end end
it "'category' should be at 16th position" do
expect(SiteSettings::TypeSupervisor.types[:category]).to eq(16)
end
it "'uploaded_image_list' should be at 17th position" do
expect(SiteSettings::TypeSupervisor.types[:uploaded_image_list]).to eq(17)
end
end end
end end

View File

@ -46,12 +46,27 @@ describe Jobs::CleanUpUploads do
it "does not clean up uploads in site settings" do it "does not clean up uploads in site settings" do
logo_upload = fabricate_upload logo_upload = fabricate_upload
logo_small_upload = fabricate_upload
favicon_upload = fabricate_upload
apple_touch_icon_upload = fabricate_upload
avatar1_upload = fabricate_upload
avatar2_upload = fabricate_upload
SiteSetting.logo_url = logo_upload.url SiteSetting.logo_url = logo_upload.url
SiteSetting.logo_small_url = logo_small_upload.url
SiteSetting.favicon_url = favicon_upload.url
SiteSetting.apple_touch_icon_url = apple_touch_icon_upload.url
SiteSetting.selectable_avatars = [avatar1_upload.url, avatar2_upload.url].join("\n")
Jobs::CleanUpUploads.new.execute(nil) Jobs::CleanUpUploads.new.execute(nil)
expect(Upload.exists?(id: @upload.id)).to eq(false) expect(Upload.exists?(id: @upload.id)).to eq(false)
expect(Upload.exists?(id: logo_upload.id)).to eq(true) expect(Upload.exists?(id: logo_upload.id)).to eq(true)
expect(Upload.exists?(id: logo_small_upload.id)).to eq(true)
expect(Upload.exists?(id: favicon_upload.id)).to eq(true)
expect(Upload.exists?(id: apple_touch_icon_upload.id)).to eq(true)
expect(Upload.exists?(id: avatar1_upload.id)).to eq(true)
expect(Upload.exists?(id: avatar2_upload.id)).to eq(true)
end end
it "does not clean up uploads in site settings when they use the CDN" do it "does not clean up uploads in site settings when they use the CDN" do

View File

@ -1771,4 +1771,18 @@ describe User do
end end
end end
describe "set_random_avatar" do
it "sets a random avatar when selectable avatars is enabled" do
avatar1 = Fabricate(:upload)
avatar2 = Fabricate(:upload)
SiteSetting.selectable_avatars_enabled = true
SiteSetting.selectable_avatars = [avatar1.url, avatar2.url].join("\n")
user = Fabricate(:user)
expect(user.uploaded_avatar_id).not_to be(nil)
expect([avatar1.id, avatar2.id]).to include(user.uploaded_avatar_id)
expect(user.user_avatar.custom_upload_id).to eq(user.uploaded_avatar_id)
end
end
end end

View File

@ -56,4 +56,30 @@ describe SiteController do
expect(response).to redirect_to '/' expect(response).to redirect_to '/'
end end
end end
describe '.selectable_avatars' do
before do
SiteSetting.selectable_avatars = "https://www.discourse.org\nhttps://meta.discourse.org"
end
it 'returns empty array when selectable avatars is disabled' do
SiteSetting.selectable_avatars_enabled = false
get "/site/selectable-avatars.json"
json = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(json).to eq([])
end
it 'returns an array when selectable avatars is enabled' do
SiteSetting.selectable_avatars_enabled = true
get "/site/selectable-avatars.json"
json = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(json).to contain_exactly("https://www.discourse.org", "https://meta.discourse.org")
end
end
end end

View File

@ -1785,6 +1785,61 @@ describe UsersController do
end end
end end
describe '#select_avatar' do
it 'raises an error when not logged in' do
put "/u/asdf/preferences/avatar/select.json", params: { url: "https://meta.discourse.org" }
expect(response.status).to eq(403)
end
context 'while logged in' do
let!(:user) { sign_in(Fabricate(:user)) }
let(:avatar1) { Fabricate(:upload) }
let(:avatar2) { Fabricate(:upload) }
let(:url) { "https://www.discourse.org" }
it 'raises an error when url is blank' do
put "/u/#{user.username}/preferences/avatar/select.json", params: { url: "" }
expect(response.status).to eq(422)
end
it 'raises an error when selectable avatars is disabled' do
put "/u/#{user.username}/preferences/avatar/select.json", params: { url: url }
expect(response.status).to eq(422)
end
context 'selectable avatars is enabled' do
before { SiteSetting.selectable_avatars_enabled = true }
it 'raises an error when selectable avatars is empty' do
put "/u/#{user.username}/preferences/avatar/select.json", params: { url: url }
expect(response.status).to eq(422)
end
context 'selectable avatars is properly setup' do
before do
SiteSetting.selectable_avatars = [avatar1.url, avatar2.url].join("\n")
end
it 'raises an error when url is not in selectable avatars list' do
put "/u/#{user.username}/preferences/avatar/select.json", params: { url: url }
expect(response.status).to eq(422)
end
it 'can successfully select an avatar' do
put "/u/#{user.username}/preferences/avatar/select.json", params: { url: avatar1.url }
expect(response.status).to eq(200)
expect(user.reload.uploaded_avatar_id).to eq(avatar1.id)
expect(user.user_avatar.reload.custom_upload_id).to eq(avatar1.id)
end
end
end
end
end
describe '#destroy_user_image' do describe '#destroy_user_image' do
it 'raises an error when not logged in' do it 'raises an error when not logged in' do

View File

@ -163,11 +163,41 @@ QUnit.test("second factor backup", assert => {
}); });
}); });
QUnit.test("default avatar selector", assert => {
visit("/u/eviltrout/preferences");
click(".pref-avatar .btn");
andThen(() => {
assert.ok(exists(".avatar-choice", "opens the avatar selection modal"));
});
});
acceptance("Avatar selector when selectable avatars is enabled", {
loggedIn: true,
settings: { selectable_avatars_enabled: true },
beforeEach() {
// prettier-ignore
server.get("/site/selectable-avatars.json", () => { //eslint-disable-line
return [200, { "Content-Type": "application/json" }, [
"https://www.discourse.org",
"https://meta.discourse.org",
]];
});
}
});
QUnit.test("selectable avatars", assert => {
visit("/u/eviltrout/preferences");
click(".pref-avatar .btn");
andThen(() => {
assert.ok(exists(".selectable-avatars", "opens the avatar selection modal"));
});
});
acceptance("User Preferences when badges are disabled", { acceptance("User Preferences when badges are disabled", {
loggedIn: true, loggedIn: true,
settings: { settings: { enable_badges: false }
enable_badges: false
}
}); });
QUnit.test("visit my preferences", assert => { QUnit.test("visit my preferences", assert => {