mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 17:51:16 +08:00
FEATURE: selectable avatars
This commit is contained in:
@ -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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -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({
|
||||||
|
@ -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>
|
@ -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>
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
@ -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, {
|
||||||
|
@ -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) {
|
||||||
|
@ -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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
<label class="btn" disabled={{uploading}} title="{{i18n "admin.site_settings.uploaded_image_list.upload.title"}}">
|
||||||
|
{{d-icon "picture-o"}} {{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}}
|
@ -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}}
|
||||||
|
@ -106,3 +106,4 @@
|
|||||||
color: $danger;
|
color: $danger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 &&
|
||||||
|
@ -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'
|
||||||
|
@ -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)."
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 => {
|
||||||
|
Reference in New Issue
Block a user