From 6d6e026e3c5120da04d90a63e8f631e9a3100198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 18 Jul 2018 12:57:43 +0200 Subject: [PATCH] FEATURE: selectable avatars --- .../site-settings/uploaded-image-list.js.es6 | 15 ++++ .../modals/admin-uploaded-image-list.js.es6 | 27 +++++++ .../admin/mixins/setting-component.js.es6 | 3 +- .../site-settings/uploaded-image-list.hbs | 2 + .../modal/admin-uploaded-image-list.hbs | 15 ++++ .../components/images-uploader.js.es6 | 20 +++++ .../controllers/avatar-selector.js.es6 | 1 - .../javascripts/discourse/models/user.js.es6 | 18 +++-- .../discourse/routes/preferences.js.es6 | 45 ++++++----- .../templates/components/images-uploader.hbs | 7 ++ .../templates/modal/avatar-selector.hbs | 76 +++++++++++-------- .../stylesheets/common/admin/settings.scss | 1 + app/assets/stylesheets/common/base/user.scss | 17 +++++ app/controllers/site_controller.rb | 10 +++ app/controllers/users_controller.rb | 45 ++++++++++- app/jobs/scheduled/clean_up_uploads.rb | 3 +- app/models/user.rb | 36 ++++++--- config/locales/client.en.yml | 8 ++ config/locales/server.en.yml | 3 + config/routes.rb | 2 + config/site_settings.yml | 6 ++ lib/site_settings/type_supervisor.rb | 3 +- .../site_settings/type_supervisor_spec.rb | 6 ++ spec/jobs/clean_up_uploads_spec.rb | 15 ++++ spec/models/user_spec.rb | 14 ++++ spec/requests/site_controller_spec.rb | 26 +++++++ spec/requests/users_controller_spec.rb | 55 ++++++++++++++ .../acceptance/preferences-test.js.es6 | 36 ++++++++- 28 files changed, 435 insertions(+), 80 deletions(-) create mode 100644 app/assets/javascripts/admin/components/site-settings/uploaded-image-list.js.es6 create mode 100644 app/assets/javascripts/admin/controllers/modals/admin-uploaded-image-list.js.es6 create mode 100644 app/assets/javascripts/admin/templates/components/site-settings/uploaded-image-list.hbs create mode 100644 app/assets/javascripts/admin/templates/modal/admin-uploaded-image-list.hbs create mode 100644 app/assets/javascripts/discourse/components/images-uploader.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/images-uploader.hbs diff --git a/app/assets/javascripts/admin/components/site-settings/uploaded-image-list.js.es6 b/app/assets/javascripts/admin/components/site-settings/uploaded-image-list.js.es6 new file mode 100644 index 00000000000..31a659a054a --- /dev/null +++ b/app/assets/javascripts/admin/components/site-settings/uploaded-image-list.js.es6 @@ -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) + }); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-uploaded-image-list.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-uploaded-image-list.js.es6 new file mode 100644 index 00000000000..2fe70259c83 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-uploaded-image-list.js.es6 @@ -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"); + } + } +}); diff --git a/app/assets/javascripts/admin/mixins/setting-component.js.es6 b/app/assets/javascripts/admin/mixins/setting-component.js.es6 index 8f50892f9e8..997917def11 100644 --- a/app/assets/javascripts/admin/mixins/setting-component.js.es6 +++ b/app/assets/javascripts/admin/mixins/setting-component.js.es6 @@ -9,7 +9,8 @@ const CUSTOM_TYPES = [ "host_list", "category_list", "value_list", - "category" + "category", + "uploaded_image_list", ]; export default Ember.Mixin.create({ diff --git a/app/assets/javascripts/admin/templates/components/site-settings/uploaded-image-list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/uploaded-image-list.hbs new file mode 100644 index 00000000000..3de2abc489a --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/site-settings/uploaded-image-list.hbs @@ -0,0 +1,2 @@ +{{d-button label="admin.site_settings.uploaded_image_list.label" action="showUploadModal" actionParam=(hash value=value setting=setting)}} +
{{{unbound setting.description}}}
diff --git a/app/assets/javascripts/admin/templates/modal/admin-uploaded-image-list.hbs b/app/assets/javascripts/admin/templates/modal/admin-uploaded-image-list.hbs new file mode 100644 index 00000000000..72d0fe94b89 --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin-uploaded-image-list.hbs @@ -0,0 +1,15 @@ +{{#d-modal-body class="uploaded-image-list"}} +
+ {{#each images as |image|}} +
+ {{bound-avatar-template image "huge"}} +
+ {{else}} +

{{i18n "admin.site_settings.uploaded_image_list.empty"}}

+ {{/each}} +
+{{/d-modal-body}} + diff --git a/app/assets/javascripts/discourse/components/images-uploader.js.es6 b/app/assets/javascripts/discourse/components/images-uploader.js.es6 new file mode 100644 index 00000000000..a4114b77ad4 --- /dev/null +++ b/app/assets/javascripts/discourse/components/images-uploader.js.es6 @@ -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); + }, +}); diff --git a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 index 3b1ecabbdeb..58e24754844 100644 --- a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 @@ -1,7 +1,6 @@ import computed from "ember-addons/ember-computed-decorators"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { ajax } from "discourse/lib/ajax"; - import { allowsImages } from "discourse/lib/utilities"; export default Ember.Controller.extend(ModalFunctionality, { diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 2d42c14c8db..f1578bfe710 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -508,19 +508,21 @@ const User = RestModel.extend({ data: { upload_id, type } } ).then(() => - this.setProperties({ - avatar_template, - uploaded_avatar_id: upload_id - }) + this.setProperties({ 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) { - return ( - this.get("staff") || + return this.get("staff") || 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) { diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index 308aadf3dcf..e49c0125160 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -1,23 +1,15 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; import showModal from "discourse/lib/show-modal"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { ajax } from "discourse/lib/ajax"; export default RestrictedUserRoute.extend({ model() { return this.modelFor("user"); }, - setupController(controller, user) { - controller.setProperties({ - model: user - }); - }, - actions: { showAvatarSelector() { - showModal("avatar-selector"); - - // all the properties needed for displaying the avatar selector modal const props = this.modelFor("user").getProperties( "id", "email", @@ -42,15 +34,32 @@ export default RestrictedUserRoute.extend({ 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() { - const user = this.modelFor("user"), - controller = this.controllerFor("avatar-selector"), - selectedUploadId = controller.get("selectedUploadId"), - selectedAvatarTemplate = controller.get("selectedAvatarTemplate"), - type = controller.get("selected"); + const user = this.modelFor("user"); + const controller = this.controllerFor("avatar-selector"); + const selectedUploadId = controller.get("selectedUploadId"); + const selectedAvatarTemplate = controller.get("selectedAvatarTemplate"); + const type = controller.get("selected"); user .pickAvatar(selectedUploadId, type, selectedAvatarTemplate) @@ -64,10 +73,8 @@ export default RestrictedUserRoute.extend({ ); bootbox.alert(I18n.t("user.change_avatar.cache_notice")); }) - .catch(popupAjaxError); - - // saves the data back - controller.send("closeModal"); + .catch(popupAjaxError) + .finally(() => controller.send("closeModal")); } } }); diff --git a/app/assets/javascripts/discourse/templates/components/images-uploader.hbs b/app/assets/javascripts/discourse/templates/components/images-uploader.hbs new file mode 100644 index 00000000000..a8a3f7a3812 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/images-uploader.hbs @@ -0,0 +1,7 @@ + +{{#if uploading}} + {{i18n 'upload_selector.uploading'}} {{uploadProgress}}% +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs b/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs index f974216e931..e2a34afac3b 100644 --- a/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs +++ b/app/assets/javascripts/discourse/templates/modal/avatar-selector.hbs @@ -1,37 +1,49 @@ {{#d-modal-body title="user.change_avatar.title" class="avatar-selector"}} -
- {{radio-button id="system-avatar" name="avatar" value="system" selection=selected}} - -
-
- {{radio-button id="gravatar" name="avatar" value="gravatar" selection=selected}} - - {{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}} - {{#if gravatarFailed}} -

{{I18n 'user.change_avatar.gravatar_failed'}}

- {{/if}} -
- {{#if allowAvatarUpload}} -
- {{radio-button id="uploaded-avatar" name="avatar" value="uploaded" selection=selected}} - - {{avatar-uploader user_id=id - uploadedAvatarTemplate=custom_avatar_template - uploadedAvatarId=custom_avatar_upload_id - uploading=uploading - done="uploadComplete"}} + {{#if siteSettings.selectable_avatars_enabled}} +
+ {{#each selectableAvatars as |avatar|}} +
+ {{bound-avatar-template avatar "huge"}} +
+ {{/each}}
+ {{else}} +
+ {{radio-button id="system-avatar" name="avatar" value="system" selection=selected}} + +
+
+ {{radio-button id="gravatar" name="avatar" value="gravatar" selection=selected}} + + {{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}} + {{#if gravatarFailed}} +

{{I18n 'user.change_avatar.gravatar_failed'}}

+ {{/if}} +
+ {{#if allowAvatarUpload}} +
+ {{radio-button id="uploaded-avatar" name="avatar" value="uploaded" selection=selected}} + + {{avatar-uploader user_id=id + uploadedAvatarTemplate=custom_avatar_template + uploadedAvatarId=custom_avatar_upload_id + uploading=uploading + done="uploadComplete"}} +
+ {{/if}} {{/if}} {{/d-modal-body}} - +{{#unless siteSettings.selectable_avatars_enabled}} + +{{/unless}} diff --git a/app/assets/stylesheets/common/admin/settings.scss b/app/assets/stylesheets/common/admin/settings.scss index 90df1c5e43d..9c40a476b1d 100644 --- a/app/assets/stylesheets/common/admin/settings.scss +++ b/app/assets/stylesheets/common/admin/settings.scss @@ -106,3 +106,4 @@ color: $danger; } } + diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index 6170668509c..96a503612da 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -661,3 +661,20 @@ #user-card .staged { 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; + } + } + } +} diff --git a/app/controllers/site_controller.rb b/app/controllers/site_controller.rb index 1e622449c95..42d19813af9 100644 --- a/app/controllers/site_controller.rb +++ b/app/controllers/site_controller.rb @@ -25,6 +25,16 @@ class SiteController < ApplicationController render json: custom_emoji 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 results = { logo_url: UrlHelper.absolute(SiteSetting.logo_url), diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8880d6304b3..6382c4bafb1 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -11,8 +11,9 @@ class UsersController < ApplicationController requires_login only: [ :username, :update, :user_preferences_redirect, :upload_user_image, - :pick_avatar, :destroy_user_image, :destroy, :check_emails, :topic_tracking_state, - :preferences, :create_second_factor, :update_second_factor, :create_second_factor_backup + :pick_avatar, :destroy_user_image, :destroy, :check_emails, + :topic_tracking_state, :preferences, :create_second_factor, + :update_second_factor, :create_second_factor_backup, :select_avatar ] skip_before_action :check_xhr, only: [ @@ -885,6 +886,46 @@ class UsersController < ApplicationController render json: success_json 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 user = fetch_user_from_params guardian.ensure_can_edit!(user) diff --git a/app/jobs/scheduled/clean_up_uploads.rb b/app/jobs/scheduled/clean_up_uploads.rb index a4bb083ce33..09e1624ad77 100644 --- a/app/jobs/scheduled/clean_up_uploads.rb +++ b/app/jobs/scheduled/clean_up_uploads.rb @@ -24,7 +24,8 @@ module Jobs SiteSetting.logo_small_url, SiteSetting.favicon_url, SiteSetting.apple_touch_icon_url, - ].map do |url| + *SiteSetting.selectable_avatars.split("\n"), + ].flatten.map do |url| if url.present? url = url.dup diff --git a/app/models/user.rb b/app/models/user.rb index 8cfa1bad3ad..019567b2a86 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -104,6 +104,7 @@ class User < ActiveRecord::Base after_create :create_user_stat after_create :create_user_option after_create :create_user_profile + after_create :set_random_avatar after_create :ensure_in_trust_level_group after_create :set_default_categories_preferences @@ -612,8 +613,7 @@ class User < ActiveRecord::Base end def self.gravatar_template(email) - email_hash = self.email_hash(email) - "//www.gravatar.com/avatar/#{email_hash}.png?s={size}&r=pg&d=identicon" + "//www.gravatar.com/avatar/#{self.email_hash(email)}.png?s={size}&r=pg&d=identicon" end # 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 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) if SiteSetting.default_avatars.present? - split_avatars = SiteSetting.default_avatars.split("\n") - if split_avatars.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) + urls = SiteSetting.default_avatars.split("\n") + return urls[username_hash(username) % urls.size] if urls.present? end + + system_avatar_template(username) end def self.avatar_template(username, uploaded_avatar_id) @@ -1018,6 +1018,18 @@ class User < ActiveRecord::Base UserProfile.create(user_id: id) 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? SiteSetting.allow_anonymous_posting && trust_level >= 1 && diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 37121640b2c..c9fd0e40ecf 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3797,6 +3797,14 @@ en: clear_filter: "Clear" add_url: "add URL" 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: all_results: 'All' required: 'Required' diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index c69fd69cc8c..de9dbd06363 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1339,6 +1339,9 @@ en: 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}" + 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." 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)." diff --git a/config/routes.rb b/config/routes.rb index 3793b7baed5..79536cbf4f8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,6 +54,7 @@ Discourse::Application.routes.draw do get "site/basic-info" => 'site#basic_info' get "site/statistics" => 'site#statistics' + get "site/selectable-avatars" => "site#selectable_avatars" 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 } 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/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/summary" => "users#summary", constraints: { username: RouteFormat.username } get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username } diff --git a/config/site_settings.yml b/config/site_settings.yml index 68f64d09f7f..e808918656f 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -959,6 +959,12 @@ files: client: true regex: '^((https?:)?\/)?\/.+[^\/]' 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 png_to_jpg_quality: default: 95 diff --git a/lib/site_settings/type_supervisor.rb b/lib/site_settings/type_supervisor.rb index 3e7c0ec3f13..caf34f62002 100644 --- a/lib/site_settings/type_supervisor.rb +++ b/lib/site_settings/type_supervisor.rb @@ -29,7 +29,8 @@ class SiteSettings::TypeSupervisor regex: 13, email: 14, username: 15, - category: 16 + category: 16, + uploaded_image_list: 17, ) end diff --git a/spec/components/site_settings/type_supervisor_spec.rb b/spec/components/site_settings/type_supervisor_spec.rb index 3e4fbae1cd9..e404213508b 100644 --- a/spec/components/site_settings/type_supervisor_spec.rb +++ b/spec/components/site_settings/type_supervisor_spec.rb @@ -73,6 +73,12 @@ describe SiteSettings::TypeSupervisor do it "'username' should be at 15th position" do expect(SiteSettings::TypeSupervisor.types[:username]).to eq(15) 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 diff --git a/spec/jobs/clean_up_uploads_spec.rb b/spec/jobs/clean_up_uploads_spec.rb index b0424058211..476e6d8e3cd 100644 --- a/spec/jobs/clean_up_uploads_spec.rb +++ b/spec/jobs/clean_up_uploads_spec.rb @@ -46,12 +46,27 @@ describe Jobs::CleanUpUploads do it "does not clean up uploads in site settings" do 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_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) expect(Upload.exists?(id: @upload.id)).to eq(false) 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 it "does not clean up uploads in site settings when they use the CDN" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e91c3e86c8c..a8ed543876a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1771,4 +1771,18 @@ describe User do 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 diff --git a/spec/requests/site_controller_spec.rb b/spec/requests/site_controller_spec.rb index b084dec7b19..433c685ae9b 100644 --- a/spec/requests/site_controller_spec.rb +++ b/spec/requests/site_controller_spec.rb @@ -56,4 +56,30 @@ describe SiteController do expect(response).to redirect_to '/' 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 diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 3fd4f18a9f5..9b213c5aaba 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -1785,6 +1785,61 @@ describe UsersController do 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 it 'raises an error when not logged in' do diff --git a/test/javascripts/acceptance/preferences-test.js.es6 b/test/javascripts/acceptance/preferences-test.js.es6 index b70b13a061f..bf0f1ea68c7 100644 --- a/test/javascripts/acceptance/preferences-test.js.es6 +++ b/test/javascripts/acceptance/preferences-test.js.es6 @@ -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", { loggedIn: true, - settings: { - enable_badges: false - } + settings: { enable_badges: false } }); QUnit.test("visit my preferences", assert => {